mirror of
https://github.com/thepeacockproject/Peacock
synced 2024-11-16 11:03:30 +01:00
Initial commit
Co-authored-by: Tino Roivanen <tino.roivanen98@gmail.com> Co-authored-by: Govert de Gans <grappigegovert@hotmail.com> Co-authored-by: Gray Olson <gray@grayolson.com> Co-authored-by: Alexandre Sanchez <alex73630@gmail.com> Co-authored-by: Anthony Fuller <24512050+anthonyfuller@users.noreply.github.com> Co-authored-by: atampy25 <24306974+atampy25@users.noreply.github.com> Co-authored-by: David <davidstulemeijer@gmail.com> Co-authored-by: c0derMo <c0dermo@users.noreply.github.com> Co-authored-by: Jeevat Singh <jeevatt.singh@gmail.com> Signed-off-by: Reece Dunham <me@rdil.rocks>
This commit is contained in:
commit
6245e91624
73
.cirrus.yml
Normal file
73
.cirrus.yml
Normal file
@ -0,0 +1,73 @@
|
||||
env:
|
||||
# don't clone the entire repository history, saves time
|
||||
CIRRUS_CLONE_DEPTH: 1
|
||||
|
||||
Patcher_task:
|
||||
# we don't need to tell GitHub if this isn't done
|
||||
skip_notifications: "!changesInclude('.cirrus.yml', '**.{cs,resx,config,settings,ico,csproj,sln}')"
|
||||
# skip if patcher remains unchanged
|
||||
skip: "!changesInclude('.cirrus.yml', '**.{cs,resx,config,settings,ico,csproj,sln}')"
|
||||
windows_container:
|
||||
image: cirrusci/windowsservercore:visualstudio2019
|
||||
Build_script:
|
||||
- patcher/BuildCI.cmd
|
||||
Patcher_Windows_artifacts:
|
||||
path: patcher/bin/x64/Release/PeacockPatcher.exe
|
||||
type: application/vnd.microsoft.portable-executable
|
||||
Patcher_Linux_artifacts:
|
||||
path: patcher/bin/x64/Linux.Release/PeacockPatcher.exe
|
||||
type: application/vnd.microsoft.portable-executable
|
||||
|
||||
Build_task:
|
||||
container:
|
||||
image: node:18-slim
|
||||
cpu: 4
|
||||
memory: 4gb
|
||||
Yarn_cache:
|
||||
folder: .yarn/cache
|
||||
Yarn_Populate_script:
|
||||
- yarn
|
||||
Install_System_Dependencies_script:
|
||||
- apt update --yes
|
||||
- apt install zip jq --yes
|
||||
Build_Modules_script:
|
||||
- yarn build
|
||||
Optimize_script:
|
||||
- yarn optimize
|
||||
Assemble_Full_script:
|
||||
- ./packaging/ciAssemble.sh
|
||||
Assemble_Lite_script:
|
||||
- ./packaging/ciAssemble.sh lite
|
||||
Peacock_Release_artifacts:
|
||||
path: Peacock-v*.zip
|
||||
type: application/zip
|
||||
SourceMap_artifacts:
|
||||
path: chunk0.js.map
|
||||
|
||||
task:
|
||||
container:
|
||||
image: node:18-slim
|
||||
Yarn_cache:
|
||||
folder: .yarn/cache
|
||||
Yarn_Populate_script:
|
||||
- yarn
|
||||
Type_Check_script:
|
||||
- yarn typecheck
|
||||
matrix:
|
||||
- only_if: $CIRRUS_TAG != ''
|
||||
name: Types Publish
|
||||
env:
|
||||
NPM_AUTH_TOKEN: ENCRYPTED[f8bceee158d69277081075585ade5affc81890ef5f66385d9ae50ab6a75fa3a27a5501ecb8dadc95a6e4e3cace0b915d]
|
||||
Install_System_Dependencies_script:
|
||||
- apt update --yes
|
||||
- apt install rsync --yes
|
||||
Push_Types_script:
|
||||
- rsync -a --include '*/' --include '*.d.ts' --exclude '*' build/ packaging/typedefs/
|
||||
- yarn typedefs reversion
|
||||
- yarn typedefs pack
|
||||
- yarn config set npmAuthToken $NPM_AUTH_TOKEN
|
||||
- yarn typedefs npm publish
|
||||
- only_if: $CIRRUS_TAG == ''
|
||||
name: Linting
|
||||
Lint_script:
|
||||
- yarn lint
|
12
.editorconfig
Normal file
12
.editorconfig
Normal file
@ -0,0 +1,12 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
max_line_length = 80
|
||||
|
||||
[*.xml]
|
||||
insert_final_newline = false
|
||||
indent_size = 2
|
11
.eslintignore
Normal file
11
.eslintignore
Normal file
@ -0,0 +1,11 @@
|
||||
packaging
|
||||
*.d.ts
|
||||
build
|
||||
.eslintrc.js
|
||||
chunk*.js
|
||||
chunk*.mjs
|
||||
webui/dist
|
||||
*.plugin.js
|
||||
*Plugin.js
|
||||
packaging/livesplit-node-client/build
|
||||
webstorm.config.js
|
64
.eslintrc.js
Normal file
64
.eslintrc.js
Normal file
@ -0,0 +1,64 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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/>.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
env: {
|
||||
node: true,
|
||||
es2021: true,
|
||||
},
|
||||
plugins: ["@typescript-eslint", "promise", "react-hooks"],
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier",
|
||||
],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
ecmaVersion: "es2022",
|
||||
sourceType: "module",
|
||||
project: [
|
||||
// server full
|
||||
"./tsconfig.json",
|
||||
// web UI
|
||||
"./webui/tsconfig.json",
|
||||
],
|
||||
},
|
||||
rules: {
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-extra-semi": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/require-await": "warn",
|
||||
"@typescript-eslint/prefer-ts-expect-error": "error",
|
||||
eqeqeq: "error",
|
||||
"no-duplicate-imports": "warn",
|
||||
"promise/always-return": "error",
|
||||
"promise/no-return-wrap": "error",
|
||||
"promise/param-names": "error",
|
||||
"promise/catch-or-return": "error",
|
||||
"promise/no-native": "off",
|
||||
"promise/no-nesting": "warn",
|
||||
"promise/no-promise-in-callback": "warn",
|
||||
"promise/no-callback-in-promise": "warn",
|
||||
"promise/avoid-new": "off",
|
||||
"promise/no-new-statics": "error",
|
||||
"promise/no-return-in-finally": "warn",
|
||||
"promise/valid-params": "warn",
|
||||
"react-hooks/rules-of-hooks": "error",
|
||||
"react-hooks/exhaustive-deps": "warn",
|
||||
},
|
||||
}
|
3
.gitattributes
vendored
Normal file
3
.gitattributes
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
* text=auto eol=lf
|
||||
*.{png,jpg,exe} binary
|
||||
.yarn/releases/*.cjs linguist-generated
|
38
.gitignore
vendored
Normal file
38
.gitignore
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
*/.DS_Store
|
||||
|
||||
contracts/*.ocre
|
||||
|
||||
/chunk*.js
|
||||
|
||||
build
|
||||
/.idea/workspace.xml
|
||||
contractSessions
|
||||
|
||||
*.map
|
||||
|
||||
userdata
|
||||
peacock_patcher.conf
|
||||
|
||||
resources/rpkg-cli.exe
|
||||
|
||||
components/contracts.json
|
||||
DEBUG_PROFILE.json
|
||||
|
||||
assembled
|
||||
webui/dist
|
||||
|
||||
.yarn/cache
|
||||
.yarn/install-state.gz
|
||||
.yarn/unplugged
|
||||
options.ini
|
||||
|
||||
*.tsbuildinfo
|
||||
|
||||
images
|
||||
/.idea/shelf/
|
||||
offlineassets.zip
|
||||
|
||||
resources/contracts.br
|
||||
resources/challenges
|
5
.idea/.gitignore
vendored
Normal file
5
.idea/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
40
.idea/Peacock.iml
Normal file
40
.idea/Peacock.iml
Normal file
@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/components" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/contractdata" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/patcher" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/static" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/webui" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/contractSessions" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/contracts" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/userdata" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/resources/dynamic_resources_h1" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/resources/dynamic_resources_h2" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/resources/dynamic_resources_h3" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/resources/LOCR" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/patcher/.vs" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/webui/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/assembled" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.yarn/cache" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.yarn/releases" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.yarn/plugins" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/patcher/obj" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/livesplit-node-client/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/images/actors" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.vscode" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.yarn/unplugged" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/patcher/bin" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/images/sarajevo_six" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/resources/challenges" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/patcher/.idea/.idea.HitmanPatcher/.idea/shelf" />
|
||||
<excludePattern pattern="chunk*.js" />
|
||||
<excludePattern pattern="components/contracts.json,*.tsbuildinfo,offlineassets.zip,chunk0.js.map" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
61
.idea/codeStyles/Project.xml
Normal file
61
.idea/codeStyles/Project.xml
Normal file
@ -0,0 +1,61 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<HTMLCodeStyleSettings>
|
||||
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
|
||||
<option name="HTML_ENFORCE_QUOTES" value="true" />
|
||||
</HTMLCodeStyleSettings>
|
||||
<JSCodeStyleSettings version="0">
|
||||
<option name="USE_SEMICOLON_AFTER_STATEMENT" value="false" />
|
||||
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
||||
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||
<option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
|
||||
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||
</JSCodeStyleSettings>
|
||||
<TypeScriptCodeStyleSettings version="0">
|
||||
<option name="USE_SEMICOLON_AFTER_STATEMENT" value="false" />
|
||||
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
||||
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||
<option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
|
||||
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||
<option name="USE_PATH_MAPPING" value="NEVER" />
|
||||
</TypeScriptCodeStyleSettings>
|
||||
<VueCodeStyleSettings>
|
||||
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
|
||||
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
|
||||
</VueCodeStyleSettings>
|
||||
<codeStyleSettings language="CSS">
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="HTML">
|
||||
<option name="SOFT_MARGINS" value="80" />
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="JSON">
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="4" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="JavaScript">
|
||||
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
|
||||
<option name="SOFT_MARGINS" value="80" />
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="TypeScript">
|
||||
<option name="SOFT_MARGINS" value="80" />
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="Vue">
|
||||
<option name="SOFT_MARGINS" value="80" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="4" />
|
||||
<option name="TAB_SIZE" value="4" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
5
.idea/codeStyles/codeStyleConfig.xml
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
6
.idea/compiler.xml
Normal file
6
.idea/compiler.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="TypeScriptCompiler">
|
||||
<option name="recompileOnChanges" value="true" />
|
||||
</component>
|
||||
</project>
|
6
.idea/copyright/AGPL_3_0.xml
Normal file
6
.idea/copyright/AGPL_3_0.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<component name="CopyrightManager">
|
||||
<copyright>
|
||||
<option name="notice" value=" The Peacock Project - a HITMAN server replacement. Copyright (C) 2021-2022 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/>." />
|
||||
<option name="myName" value="AGPL-3.0" />
|
||||
</copyright>
|
||||
</component>
|
3
.idea/copyright/profiles_settings.xml
Normal file
3
.idea/copyright/profiles_settings.xml
Normal file
@ -0,0 +1,3 @@
|
||||
<component name="CopyrightManager">
|
||||
<settings default="AGPL-3.0" />
|
||||
</component>
|
59
.idea/dictionaries/reece.xml
Normal file
59
.idea/dictionaries/reece.xml
Normal file
@ -0,0 +1,59 @@
|
||||
<component name="ProjectDictionaryState">
|
||||
<dictionary name="reece">
|
||||
<words>
|
||||
<w>ascensionist</w>
|
||||
<w>bosco</w>
|
||||
<w>calluna</w>
|
||||
<w>cereus</w>
|
||||
<w>coastaltown</w>
|
||||
<w>cooldown</w>
|
||||
<w>customkill</w>
|
||||
<w>desertrose</w>
|
||||
<w>gamechanger</w>
|
||||
<w>gamechangers</w>
|
||||
<w>gartersnake</w>
|
||||
<w>grasssnake</w>
|
||||
<w>hantu</w>
|
||||
<w>herospawn</w>
|
||||
<w>invertus</w>
|
||||
<w>kashmirian</w>
|
||||
<w>killmethod</w>
|
||||
<w>kingcobra</w>
|
||||
<w>livesplit</w>
|
||||
<w>loadout</w>
|
||||
<w>lunaria</w>
|
||||
<w>makoyana</w>
|
||||
<w>movieset</w>
|
||||
<w>newzealand</w>
|
||||
<w>nightphlox</w>
|
||||
<w>northamerica</w>
|
||||
<w>orbis</w>
|
||||
<w>polarbear</w>
|
||||
<w>protea</w>
|
||||
<w>radler</w>
|
||||
<w>rafflesia</w>
|
||||
<w>redsnapper</w>
|
||||
<w>sambuca</w>
|
||||
<w>sapienza</w>
|
||||
<w>scpc</w>
|
||||
<w>setpiece</w>
|
||||
<w>setpieces</w>
|
||||
<w>sgáil</w>
|
||||
<w>sheepsorrel</w>
|
||||
<w>singleplayer</w>
|
||||
<w>situs</w>
|
||||
<w>smoothsnake</w>
|
||||
<w>snakeshead</w>
|
||||
<w>sniperrifle</w>
|
||||
<w>snowcrane</w>
|
||||
<w>specialassignment</w>
|
||||
<w>stashpoints</w>
|
||||
<w>statemachine</w>
|
||||
<w>torenia</w>
|
||||
<w>unlockable</w>
|
||||
<w>usercreated</w>
|
||||
<w>vsrace</w>
|
||||
<w>yardbird</w>
|
||||
</words>
|
||||
</dictionary>
|
||||
</component>
|
7
.idea/discord.xml
Normal file
7
.idea/discord.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DiscordProjectSettings">
|
||||
<option name="show" value="PROJECT" />
|
||||
<option name="description" value="A HITMAN trilogy server replacement." />
|
||||
</component>
|
||||
</project>
|
10
.idea/externalDependencies.xml
Normal file
10
.idea/externalDependencies.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalDependencies">
|
||||
<plugin id="JavaScript" />
|
||||
<plugin id="JavaScriptDebugger" />
|
||||
<plugin id="NodeJS" />
|
||||
<plugin id="com.intellij.microservices.ui" />
|
||||
<plugin id="intellij.prettierJS" />
|
||||
</component>
|
||||
</project>
|
45
.idea/inspectionProfiles/Project_Default.xml
Normal file
45
.idea/inspectionProfiles/Project_Default.xml
Normal file
@ -0,0 +1,45 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="ConditionalExpressionWithIdenticalBranchesJS" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="DivideByZeroJS" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="DuplicateConditionJS" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="ES6PossiblyAsyncFunction" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="EmptyFinallyBlockJS" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="ForLoopReplaceableByWhileJS" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="m_ignoreLoopsWithoutConditions" value="false" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="HttpUrlsUsage" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<scope name="Configs and Contracts" level="INFORMATION" enabled="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="IfStatementWithIdenticalBranchesJS" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="IncorrectHttpHeaderInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="customHeaders">
|
||||
<set>
|
||||
<option value="Peacock-Version" />
|
||||
</set>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
<inspection_tool class="JSConstructorReturnsPrimitive" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="JSEqualityComparisonWithCoercion.TS" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="JSXNamespaceValidation" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="NestedSwitchStatementJS" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="ObjectAllocationIgnoredJS" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="PointlessBitwiseExpressionJS" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="m_ignoreExpressionsContainingConstants" value="false" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ReuseOfLocalVariableJS" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="SpellCheckingInspection" enabled="true" level="TYPO" enabled_by_default="true">
|
||||
<scope name="Configs and Contracts" level="INFORMATION" enabled="true">
|
||||
<option name="processCode" value="true" />
|
||||
<option name="processLiterals" value="true" />
|
||||
<option name="processComments" value="false" />
|
||||
</scope>
|
||||
<option name="processCode" value="true" />
|
||||
<option name="processLiterals" value="true" />
|
||||
<option name="processComments" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="UpdateDependencyToLatestVersion" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
7
.idea/jsLibraryMappings.xml
Normal file
7
.idea/jsLibraryMappings.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JavaScriptLibraryMappings">
|
||||
<includedPredefinedLibrary name="Node.js Core" />
|
||||
<excludedPredefinedLibrary name="HTML" />
|
||||
</component>
|
||||
</project>
|
6
.idea/jsLinters/eslint.xml
Normal file
6
.idea/jsLinters/eslint.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="EslintConfiguration">
|
||||
<files-pattern value="{**/*,*}.{js,ts,jsx,tsx,html,mjs,cjs}" />
|
||||
</component>
|
||||
</project>
|
25
.idea/jsonSchemas.xml
Normal file
25
.idea/jsonSchemas.xml
Normal file
@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JsonSchemaMappingsProjectConfiguration">
|
||||
<state>
|
||||
<map>
|
||||
<entry key=".cirrus.yml">
|
||||
<value>
|
||||
<SchemaInfo>
|
||||
<option name="name" value=".cirrus.yml" />
|
||||
<option name="relativePathToSchema" value="https://json.schemastore.org/cirrus.json" />
|
||||
<option name="applicationDefined" value="true" />
|
||||
<option name="patterns">
|
||||
<list>
|
||||
<Item>
|
||||
<option name="path" value=".cirrus.yml" />
|
||||
</Item>
|
||||
</list>
|
||||
</option>
|
||||
</SchemaInfo>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</state>
|
||||
</component>
|
||||
</project>
|
9
.idea/markdown.xml
Normal file
9
.idea/markdown.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="MarkdownSettings">
|
||||
<enabledExtensions>
|
||||
<entry key="MermaidLanguageExtension" value="false" />
|
||||
<entry key="PlantUMLLanguageExtension" value="false" />
|
||||
</enabledExtensions>
|
||||
</component>
|
||||
</project>
|
4
.idea/misc.xml
Normal file
4
.idea/misc.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="1.8" project-jdk-type="JavaSDK" />
|
||||
</project>
|
8
.idea/modules.xml
Normal file
8
.idea/modules.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/Peacock.iml" filepath="$PROJECT_DIR$/.idea/Peacock.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
8
.idea/prettier.xml
Normal file
8
.idea/prettier.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="PrettierConfiguration">
|
||||
<option name="myRunOnSave" value="true" />
|
||||
<option name="myRunOnReformat" value="true" />
|
||||
<option name="myFilesPattern" value="{**/*,*}.{js,ts,jsx,tsx,json,html,css,mjs,cjs}" />
|
||||
</component>
|
||||
</project>
|
12
.idea/runConfigurations/Prettier.xml
Normal file
12
.idea/runConfigurations/Prettier.xml
Normal file
@ -0,0 +1,12 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Prettier" type="js.build_tools.npm">
|
||||
<package-json value="$PROJECT_DIR$/package.json" />
|
||||
<command value="run" />
|
||||
<scripts>
|
||||
<script value="prettier" />
|
||||
</scripts>
|
||||
<node-interpreter value="project" />
|
||||
<envs />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
8
.idea/runConfigurations/Run_Server.xml
Normal file
8
.idea/runConfigurations/Run_Server.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run Server" type="NodeJSConfigurationType" application-parameters="--hmr" node-parameters="--harmony" path-to-js-file="packaging/devLoader.mjs" working-dir="$PROJECT_DIR$">
|
||||
<EXTENSION ID="com.jetbrains.nodejs.run.NodeJSProfilingRunConfigurationExtension">
|
||||
<profiling allow-runtime-heap-snapshot="true" />
|
||||
</EXTENSION>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
8
.idea/runConfigurations/Web_UI__Attach_to_Browser.xml
Normal file
8
.idea/runConfigurations/Web_UI__Attach_to_Browser.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Web UI: Attach to Browser" type="JavascriptDebugType" uri="http://localhost:3000" useFirstLineBreakpoints="true">
|
||||
<mapping url="http://localhost:3000/@fs/$PROJECT_DIR$/node_modules" local-file="$PROJECT_DIR$/node_modules" />
|
||||
<mapping url="http://localhost:3000" local-file="$PROJECT_DIR$/webui/index.html" />
|
||||
<mapping url="http://localhost:3000/src" local-file="$PROJECT_DIR$/webui/src" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
13
.idea/runConfigurations/Web_UI__Dev_Server.xml
Normal file
13
.idea/runConfigurations/Web_UI__Dev_Server.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Web UI: Dev Server" type="js.build_tools.npm">
|
||||
<package-json value="$PROJECT_DIR$/package.json" />
|
||||
<command value="run" />
|
||||
<scripts>
|
||||
<script value="webui" />
|
||||
</scripts>
|
||||
<arguments value="start" />
|
||||
<node-interpreter value="project" />
|
||||
<envs />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
3
.idea/scopes/Configs_and_Contracts.xml
Normal file
3
.idea/scopes/Configs_and_Contracts.xml
Normal file
@ -0,0 +1,3 @@
|
||||
<component name="DependencyValidationManager">
|
||||
<scope name="Configs and Contracts" pattern="file[Peacock]:static//*||file[Peacock]:contractdata//*||file:components/contracts.json" />
|
||||
</component>
|
3
.idea/scopes/Patcher.xml
Normal file
3
.idea/scopes/Patcher.xml
Normal file
@ -0,0 +1,3 @@
|
||||
<component name="DependencyValidationManager">
|
||||
<scope name="Patcher" pattern="file[Peacock]:patcher//*" />
|
||||
</component>
|
3
.idea/scopes/Server_Source.xml
Normal file
3
.idea/scopes/Server_Source.xml
Normal file
@ -0,0 +1,3 @@
|
||||
<component name="DependencyValidationManager">
|
||||
<scope name="Server Source" pattern="file[Peacock]:components//*" />
|
||||
</component>
|
3
.idea/scopes/Web_UI.xml
Normal file
3
.idea/scopes/Web_UI.xml
Normal file
@ -0,0 +1,3 @@
|
||||
<component name="DependencyValidationManager">
|
||||
<scope name="Web UI" pattern="file[Peacock]:webui//*" />
|
||||
</component>
|
13
.idea/vcs.xml
Normal file
13
.idea/vcs.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GithubSharedProjectSettings">
|
||||
<option name="branchProtectionPatterns">
|
||||
<list>
|
||||
<option value="main" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
9
.prettierignore
Normal file
9
.prettierignore
Normal file
@ -0,0 +1,9 @@
|
||||
node_modules
|
||||
userdata
|
||||
build
|
||||
contractSessions
|
||||
chunk*.js
|
||||
webui/dist
|
||||
*.plugin.js
|
||||
*Plugin.js
|
||||
components/contracts.json
|
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
|
||||
}
|
35
.vscode/launch.json
vendored
Normal file
35
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "pwa-node",
|
||||
"request": "launch",
|
||||
"name": "Dev Server (run-dev)",
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"env": {
|
||||
"DEBUG": "peacock",
|
||||
"Path": "${workspaceFolder}/nodedist;${env:Path}"
|
||||
},
|
||||
"runtimeExecutable": "yarn",
|
||||
"runtimeArgs": ["run-dev"],
|
||||
"internalConsoleOptions": "openOnSessionStart"
|
||||
},
|
||||
{
|
||||
"type": "pwa-node",
|
||||
"request": "launch",
|
||||
"name": "Prod Server (build && launch chunk0)",
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"program": "${workspaceFolder}/chunk0.js",
|
||||
"preLaunchTask": "yarn build",
|
||||
"outFiles": ["${workspaceFolder}/chunk*.js"],
|
||||
"env": {
|
||||
"DEBUG": "peacock"
|
||||
},
|
||||
"runtimeExecutable": "${workspaceFolder}/nodedist/node",
|
||||
"internalConsoleOptions": "openOnSessionStart"
|
||||
}
|
||||
]
|
||||
}
|
9
.vscode/settings.json
vendored
Normal file
9
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"eslint.runtime": "nodedist\\node.exe",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": false
|
||||
},
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||
"npm.packageManager": "yarn",
|
||||
"eslint.packageManager": "yarn"
|
||||
}
|
51
.vscode/tasks.json
vendored
Normal file
51
.vscode/tasks.json
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "shell",
|
||||
"options": {
|
||||
"env": {
|
||||
"Path": "${workspaceFolder}/nodedist;${env:Path}"
|
||||
}
|
||||
},
|
||||
"command": "yarn build",
|
||||
"label": "yarn build",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"options": {
|
||||
"env": {
|
||||
"Path": "${workspaceFolder}/nodedist;${env:Path}"
|
||||
}
|
||||
},
|
||||
"command": "yarn optimize",
|
||||
"label": "yarn optimize",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"options": {
|
||||
"env": {
|
||||
"Path": "${workspaceFolder}/nodedist;${env:Path}"
|
||||
}
|
||||
},
|
||||
"command": "yarn install",
|
||||
"label": "yarn install",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"options": {
|
||||
"env": {
|
||||
"Path": "${workspaceFolder}/nodedist;${env:Path}"
|
||||
}
|
||||
},
|
||||
"command": "yarn typecheck",
|
||||
"label": "yarn typecheck",
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
2465
.yarn/patches/express-npm-4.18.1-842e583ae1.patch
Normal file
2465
.yarn/patches/express-npm-4.18.1-842e583ae1.patch
Normal file
File diff suppressed because it is too large
Load Diff
28
.yarn/patches/http-errors-npm-2.0.0-3f1c503428.patch
Normal file
28
.yarn/patches/http-errors-npm-2.0.0-3f1c503428.patch
Normal file
@ -0,0 +1,28 @@
|
||||
diff --git a/index.js b/index.js
|
||||
index c425f1ee9d0944b1e2274ebb78528febf563d17e..2ee7a5c40090311ac8971a62f30d838d51ae5181 100644
|
||||
--- a/index.js
|
||||
+++ b/index.js
|
||||
@@ -12,10 +12,9 @@
|
||||
* @private
|
||||
*/
|
||||
|
||||
-var deprecate = require('depd')('http-errors')
|
||||
-var setPrototypeOf = require('setprototypeof')
|
||||
+var setPrototypeOf = Object.setPrototypeOf
|
||||
var statuses = require('statuses')
|
||||
-var inherits = require('inherits')
|
||||
+var inherits = require('util').inherits
|
||||
var toIdentifier = require('toidentifier')
|
||||
|
||||
/**
|
||||
@@ -69,10 +68,6 @@ function createError () {
|
||||
}
|
||||
}
|
||||
|
||||
- if (typeof status === 'number' && (status < 400 || status >= 600)) {
|
||||
- deprecate('non-error status code; use only 4xx or 5xx status codes')
|
||||
- }
|
||||
-
|
||||
if (typeof status !== 'number' ||
|
||||
(!statuses.message[status] && (status < 400 || status >= 600))) {
|
||||
status = 500
|
86
.yarn/patches/iconv-lite-npm-0.6.3-24b8aae27e.patch
Normal file
86
.yarn/patches/iconv-lite-npm-0.6.3-24b8aae27e.patch
Normal file
@ -0,0 +1,86 @@
|
||||
diff --git a/encodings/dbcs-codec.js b/encodings/dbcs-codec.js
|
||||
index fa839170367b271072dc097d29b2c05f085e7681..03616ca08753e29ed31ceb327da25abe58de4bfe 100644
|
||||
--- a/encodings/dbcs-codec.js
|
||||
+++ b/encodings/dbcs-codec.js
|
||||
@@ -1,5 +1,4 @@
|
||||
"use strict";
|
||||
-var Buffer = require("safer-buffer").Buffer;
|
||||
|
||||
// Multibyte codec. In this scheme, a character is represented by 1 or more bytes.
|
||||
// Our codec supports UTF-16 surrogates, extensions for GB18030 and unicode sequences.
|
||||
diff --git a/encodings/internal.js b/encodings/internal.js
|
||||
index dc1074f04f11a31c0e962846f5d162eab9556d38..61f574b8121e4d8b417f3058597b1ff7c62c88f0 100644
|
||||
--- a/encodings/internal.js
|
||||
+++ b/encodings/internal.js
|
||||
@@ -1,5 +1,4 @@
|
||||
"use strict";
|
||||
-var Buffer = require("safer-buffer").Buffer;
|
||||
|
||||
// Export Node.js internal encodings.
|
||||
|
||||
diff --git a/encodings/sbcs-codec.js b/encodings/sbcs-codec.js
|
||||
index abac5ffaac97da29fa5c5d8aedf5b47763fc7c58..56d6d49746b332213177e31b15c4f0d920d70124 100644
|
||||
--- a/encodings/sbcs-codec.js
|
||||
+++ b/encodings/sbcs-codec.js
|
||||
@@ -1,5 +1,4 @@
|
||||
"use strict";
|
||||
-var Buffer = require("safer-buffer").Buffer;
|
||||
|
||||
// Single-byte codec. Needs a 'chars' string parameter that contains 256 or 128 chars that
|
||||
// correspond to encoded bytes (if 128 - then lower half is ASCII).
|
||||
diff --git a/encodings/utf16.js b/encodings/utf16.js
|
||||
index 97d066925bbd5dfaa7213e0433570a113c461f3e..df1ba233fc96924709ce7ceadead8477409cfb4f 100644
|
||||
--- a/encodings/utf16.js
|
||||
+++ b/encodings/utf16.js
|
||||
@@ -1,5 +1,4 @@
|
||||
"use strict";
|
||||
-var Buffer = require("safer-buffer").Buffer;
|
||||
|
||||
// Note: UTF16-LE (or UCS2) codec is Node.js native. See encodings/internal.js
|
||||
|
||||
diff --git a/encodings/utf32.js b/encodings/utf32.js
|
||||
index 2fa900a12eb3562e38fc9442dd3f57ea919b3c74..c80e72d32eacdde6d1fdfcece7fa4aa5f60ea742 100644
|
||||
--- a/encodings/utf32.js
|
||||
+++ b/encodings/utf32.js
|
||||
@@ -1,7 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
-var Buffer = require('safer-buffer').Buffer;
|
||||
-
|
||||
// == UTF32-LE/BE codec. ==========================================================
|
||||
|
||||
exports._utf32 = Utf32Codec;
|
||||
diff --git a/encodings/utf7.js b/encodings/utf7.js
|
||||
index eacae34d5f80d0b406ad63104406ddd5f3232f4a..ad0bf4fc39c4fab1a92724c8ab3fa283e1d42c13 100644
|
||||
--- a/encodings/utf7.js
|
||||
+++ b/encodings/utf7.js
|
||||
@@ -1,5 +1,4 @@
|
||||
"use strict";
|
||||
-var Buffer = require("safer-buffer").Buffer;
|
||||
|
||||
// UTF-7 codec, according to https://tools.ietf.org/html/rfc2152
|
||||
// See also below a UTF-7-IMAP codec, according to http://tools.ietf.org/html/rfc3501#section-5.1.3
|
||||
diff --git a/lib/index.js b/lib/index.js
|
||||
index 657701c38d243b8af1cd3d4a67056e095a0ede5e..f224ea462925af6ae4fb753e193b76eb83ed3e5d 100644
|
||||
--- a/lib/index.js
|
||||
+++ b/lib/index.js
|
||||
@@ -1,7 +1,5 @@
|
||||
"use strict";
|
||||
|
||||
-var Buffer = require("safer-buffer").Buffer;
|
||||
-
|
||||
var bomHandling = require("./bom-handling"),
|
||||
iconv = module.exports;
|
||||
|
||||
diff --git a/lib/streams.js b/lib/streams.js
|
||||
index a1506482f580162d5b3a07b1ef82fcca22b40e5a..bd00f0674054c0d9d5ddb8cb5852686da71fe5f2 100644
|
||||
--- a/lib/streams.js
|
||||
+++ b/lib/streams.js
|
||||
@@ -1,7 +1,5 @@
|
||||
"use strict";
|
||||
|
||||
-var Buffer = require("safer-buffer").Buffer;
|
||||
-
|
||||
// NOTE: Due to 'stream' module being pretty large (~100Kb, significant in browser environments),
|
||||
// we opt to dependency-inject it instead of creating a hard dependency.
|
||||
module.exports = function(stream_module) {
|
54
.yarn/patches/jsonwebtoken-npm-8.5.1-c007670b76.patch
Normal file
54
.yarn/patches/jsonwebtoken-npm-8.5.1-c007670b76.patch
Normal file
@ -0,0 +1,54 @@
|
||||
diff --git a/lib/psSupported.js b/lib/psSupported.js
|
||||
deleted file mode 100644
|
||||
index 8c04144a14cc4f68210545769f5a23c03f808063..0000000000000000000000000000000000000000
|
||||
--- a/lib/psSupported.js
|
||||
+++ /dev/null
|
||||
@@ -1,3 +0,0 @@
|
||||
-var semver = require('semver');
|
||||
-
|
||||
-module.exports = semver.satisfies(process.version, '^6.12.0 || >=8.0.0');
|
||||
diff --git a/sign.js b/sign.js
|
||||
index f649ce4ff48ffc8ce7863422e3880e78ce86c322..e275fe51ce73e403bcb26196662188f3a843d8ef 100644
|
||||
--- a/sign.js
|
||||
+++ b/sign.js
|
||||
@@ -1,5 +1,4 @@
|
||||
var timespan = require('./lib/timespan');
|
||||
-var PS_SUPPORTED = require('./lib/psSupported');
|
||||
var jws = require('jws');
|
||||
var includes = require('lodash.includes');
|
||||
var isBoolean = require('lodash.isboolean');
|
||||
@@ -10,9 +9,7 @@ var isString = require('lodash.isstring');
|
||||
var once = require('lodash.once');
|
||||
|
||||
var SUPPORTED_ALGS = ['RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'HS256', 'HS384', 'HS512', 'none']
|
||||
-if (PS_SUPPORTED) {
|
||||
- SUPPORTED_ALGS.splice(3, 0, 'PS256', 'PS384', 'PS512');
|
||||
-}
|
||||
+SUPPORTED_ALGS.splice(3, 0, 'PS256', 'PS384', 'PS512');
|
||||
|
||||
var sign_options_schema = {
|
||||
expiresIn: { isValid: function(value) { return isInteger(value) || (isString(value) && value); }, message: '"expiresIn" should be a number of seconds or string representing a timespan' },
|
||||
diff --git a/verify.js b/verify.js
|
||||
index 1df99f8dd429e392da8a0b84af7d7b02c0b36d0e..c9f7cef6263dd420259ebbac60efe64fb058c342 100644
|
||||
--- a/verify.js
|
||||
+++ b/verify.js
|
||||
@@ -3,17 +3,14 @@ var NotBeforeError = require('./lib/NotBeforeError');
|
||||
var TokenExpiredError = require('./lib/TokenExpiredError');
|
||||
var decode = require('./decode');
|
||||
var timespan = require('./lib/timespan');
|
||||
-var PS_SUPPORTED = require('./lib/psSupported');
|
||||
var jws = require('jws');
|
||||
|
||||
var PUB_KEY_ALGS = ['RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512'];
|
||||
var RSA_KEY_ALGS = ['RS256', 'RS384', 'RS512'];
|
||||
var HS_ALGS = ['HS256', 'HS384', 'HS512'];
|
||||
|
||||
-if (PS_SUPPORTED) {
|
||||
- PUB_KEY_ALGS.splice(3, 0, 'PS256', 'PS384', 'PS512');
|
||||
- RSA_KEY_ALGS.splice(3, 0, 'PS256', 'PS384', 'PS512');
|
||||
-}
|
||||
+PUB_KEY_ALGS.splice(3, 0, 'PS256', 'PS384', 'PS512');
|
||||
+RSA_KEY_ALGS.splice(3, 0, 'PS256', 'PS384', 'PS512');
|
||||
|
||||
module.exports = function (jwtString, secretOrPublicKey, options, callback) {
|
||||
if ((typeof options === 'function') && !callback) {
|
42
.yarn/patches/picocolors-npm-1.0.0-d81e0b1927.patch
Normal file
42
.yarn/patches/picocolors-npm-1.0.0-d81e0b1927.patch
Normal file
@ -0,0 +1,42 @@
|
||||
diff --git a/picocolors.browser.js b/picocolors.browser.js
|
||||
index 5eb9fbe8b5a0d60e91c88a2851b3b975d70261bf..85cdbe731e277f5a780eba50b1515db31b6c2bf7 100644
|
||||
--- a/picocolors.browser.js
|
||||
+++ b/picocolors.browser.js
|
||||
@@ -1,4 +1,4 @@
|
||||
var x=String;
|
||||
-var create=function() {return {isColorSupported:false,reset:x,bold:x,dim:x,italic:x,underline:x,inverse:x,hidden:x,strikethrough:x,black:x,red:x,green:x,yellow:x,blue:x,magenta:x,cyan:x,white:x,gray:x,bgBlack:x,bgRed:x,bgGreen:x,bgYellow:x,bgBlue:x,bgMagenta:x,bgCyan:x,bgWhite:x}};
|
||||
+var create=function() {return {isColorSupported:false,reset:x,bold:x,dim:x,italic:x,underline:x,inverse:x,hidden:x,strikethrough:x,black:x,red:x,green:x,yellow:x,blue:x,magenta:x,cyan:x,white:x,gray:x,bgBlack:x,bgRed:x,bgGreen:x,bgYellow:x,bgBlue:x,bgMagenta:x,bgCyan:x,bgWhite:x,redBright:x,greenBright:x,yellowBright:x,blueBright:x,magentaBright:x,cyanBright:x}};
|
||||
module.exports=create();
|
||||
module.exports.createColors = create;
|
||||
diff --git a/picocolors.js b/picocolors.js
|
||||
index fdb630451d89b134b069355bb97556e39b0b171a..389dee8a5d5c91c66a480f8230242b651deddce3 100644
|
||||
--- a/picocolors.js
|
||||
+++ b/picocolors.js
|
||||
@@ -52,6 +52,12 @@ let createColors = (enabled = isColorSupported) => ({
|
||||
bgMagenta: enabled ? formatter("\x1b[45m", "\x1b[49m") : String,
|
||||
bgCyan: enabled ? formatter("\x1b[46m", "\x1b[49m") : String,
|
||||
bgWhite: enabled ? formatter("\x1b[47m", "\x1b[49m") : String,
|
||||
+ redBright: enabled ? formatter("\x1b[91m", "\x1b[39m") : String,
|
||||
+ greenBright: enabled ? formatter("\x1b[92m", "\x1b[39m") : String,
|
||||
+ yellowBright: enabled ? formatter("\x1b[93m", "\x1b[39m") : String,
|
||||
+ blueBright: enabled ? formatter("\x1b[94m", "\x1b[39m") : String,
|
||||
+ magentaBright: enabled ? formatter("\x1b[95m", "\x1b[39m") : String,
|
||||
+ cyanBright: enabled ? formatter("\x1b[96m", "\x1b[39m") : String,
|
||||
})
|
||||
|
||||
module.exports = createColors()
|
||||
diff --git a/types.ts b/types.ts
|
||||
index b4bacee4909e7f562fb13f89720c8ae57c4922fc..7b8327138390bfa3c397690a62a2c269cdbac06e 100644
|
||||
--- a/types.ts
|
||||
+++ b/types.ts
|
||||
@@ -27,4 +27,10 @@ export interface Colors {
|
||||
bgMagenta: Formatter
|
||||
bgCyan: Formatter
|
||||
bgWhite: Formatter
|
||||
+ redBright: Formatter
|
||||
+ greenBright: Formatter
|
||||
+ yellowBright: Formatter
|
||||
+ blueBright: Formatter
|
||||
+ magentaBright: Formatter
|
||||
+ cyanBright: Formatter
|
||||
}
|
181
.yarn/patches/send-npm-0.18.0-faadf6353f.patch
Normal file
181
.yarn/patches/send-npm-0.18.0-faadf6353f.patch
Normal file
@ -0,0 +1,181 @@
|
||||
diff --git a/index.js b/index.js
|
||||
index 89afd7e584a50233d6948255c4a4f52edbaf297c..6fdcbe7d7883027bff5ba7b8e729ae4170e057ce 100644
|
||||
--- a/index.js
|
||||
+++ b/index.js
|
||||
@@ -14,10 +14,7 @@
|
||||
|
||||
var createError = require('http-errors')
|
||||
var debug = require('debug')('send')
|
||||
-var deprecate = require('depd')('send')
|
||||
var destroy = require('destroy')
|
||||
-var encodeUrl = require('encodeurl')
|
||||
-var escapeHtml = require('escape-html')
|
||||
var etag = require('etag')
|
||||
var fresh = require('fresh')
|
||||
var fs = require('fs')
|
||||
@@ -124,10 +121,6 @@ function SendStream (req, path, options) {
|
||||
|
||||
this._hidden = Boolean(opts.hidden)
|
||||
|
||||
- if (opts.hidden !== undefined) {
|
||||
- deprecate('hidden: use dotfiles: \'' + (this._hidden ? 'allow' : 'ignore') + '\' instead')
|
||||
- }
|
||||
-
|
||||
// legacy support
|
||||
if (opts.dotfiles === undefined) {
|
||||
this._dotfiles = undefined
|
||||
@@ -172,51 +165,6 @@ function SendStream (req, path, options) {
|
||||
|
||||
util.inherits(SendStream, Stream)
|
||||
|
||||
-/**
|
||||
- * Enable or disable etag generation.
|
||||
- *
|
||||
- * @param {Boolean} val
|
||||
- * @return {SendStream}
|
||||
- * @api public
|
||||
- */
|
||||
-
|
||||
-SendStream.prototype.etag = deprecate.function(function etag (val) {
|
||||
- this._etag = Boolean(val)
|
||||
- debug('etag %s', this._etag)
|
||||
- return this
|
||||
-}, 'send.etag: pass etag as option')
|
||||
-
|
||||
-/**
|
||||
- * Enable or disable "hidden" (dot) files.
|
||||
- *
|
||||
- * @param {Boolean} path
|
||||
- * @return {SendStream}
|
||||
- * @api public
|
||||
- */
|
||||
-
|
||||
-SendStream.prototype.hidden = deprecate.function(function hidden (val) {
|
||||
- this._hidden = Boolean(val)
|
||||
- this._dotfiles = undefined
|
||||
- debug('hidden %s', this._hidden)
|
||||
- return this
|
||||
-}, 'send.hidden: use dotfiles option')
|
||||
-
|
||||
-/**
|
||||
- * Set index `paths`, set to a falsy
|
||||
- * value to disable index support.
|
||||
- *
|
||||
- * @param {String|Boolean|Array} paths
|
||||
- * @return {SendStream}
|
||||
- * @api public
|
||||
- */
|
||||
-
|
||||
-SendStream.prototype.index = deprecate.function(function index (paths) {
|
||||
- var index = !paths ? [] : normalizeList(paths, 'paths argument')
|
||||
- debug('index %o', paths)
|
||||
- this._index = index
|
||||
- return this
|
||||
-}, 'send.index: pass index as option')
|
||||
-
|
||||
/**
|
||||
* Set root `path`.
|
||||
*
|
||||
@@ -231,31 +179,6 @@ SendStream.prototype.root = function root (path) {
|
||||
return this
|
||||
}
|
||||
|
||||
-SendStream.prototype.from = deprecate.function(SendStream.prototype.root,
|
||||
- 'send.from: pass root as option')
|
||||
-
|
||||
-SendStream.prototype.root = deprecate.function(SendStream.prototype.root,
|
||||
- 'send.root: pass root as option')
|
||||
-
|
||||
-/**
|
||||
- * Set max-age to `maxAge`.
|
||||
- *
|
||||
- * @param {Number} maxAge
|
||||
- * @return {SendStream}
|
||||
- * @api public
|
||||
- */
|
||||
-
|
||||
-SendStream.prototype.maxage = deprecate.function(function maxage (maxAge) {
|
||||
- this._maxage = typeof maxAge === 'string'
|
||||
- ? ms(maxAge)
|
||||
- : Number(maxAge)
|
||||
- this._maxage = !isNaN(this._maxage)
|
||||
- ? Math.min(Math.max(0, this._maxage), MAX_MAXAGE)
|
||||
- : 0
|
||||
- debug('max-age %d', this._maxage)
|
||||
- return this
|
||||
-}, 'send.maxage: pass maxAge as option')
|
||||
-
|
||||
/**
|
||||
* Emit error with `status`.
|
||||
*
|
||||
@@ -272,7 +195,7 @@ SendStream.prototype.error = function error (status, err) {
|
||||
|
||||
var res = this.res
|
||||
var msg = statuses.message[status] || String(status)
|
||||
- var doc = createHtmlDocument('Error', escapeHtml(msg))
|
||||
+ var doc = `Error: ${msg}`
|
||||
|
||||
// clear existing headers
|
||||
clearHeaders(res)
|
||||
@@ -284,7 +207,7 @@ SendStream.prototype.error = function error (status, err) {
|
||||
|
||||
// send basic response
|
||||
res.statusCode = status
|
||||
- res.setHeader('Content-Type', 'text/html; charset=UTF-8')
|
||||
+ res.setHeader('Content-Type', 'text/plain; charset=UTF-8')
|
||||
res.setHeader('Content-Length', Buffer.byteLength(doc))
|
||||
res.setHeader('Content-Security-Policy', "default-src 'none'")
|
||||
res.setHeader('X-Content-Type-Options', 'nosniff')
|
||||
@@ -476,23 +399,7 @@ SendStream.prototype.redirect = function redirect (path) {
|
||||
return
|
||||
}
|
||||
|
||||
- if (this.hasTrailingSlash()) {
|
||||
- this.error(403)
|
||||
- return
|
||||
- }
|
||||
-
|
||||
- var loc = encodeUrl(collapseLeadingSlashes(this.path + '/'))
|
||||
- var doc = createHtmlDocument('Redirecting', 'Redirecting to <a href="' + escapeHtml(loc) + '">' +
|
||||
- escapeHtml(loc) + '</a>')
|
||||
-
|
||||
- // redirect
|
||||
- res.statusCode = 301
|
||||
- res.setHeader('Content-Type', 'text/html; charset=UTF-8')
|
||||
- res.setHeader('Content-Length', Buffer.byteLength(doc))
|
||||
- res.setHeader('Content-Security-Policy', "default-src 'none'")
|
||||
- res.setHeader('X-Content-Type-Options', 'nosniff')
|
||||
- res.setHeader('Location', loc)
|
||||
- res.end(doc)
|
||||
+ this.error(403)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -945,27 +852,6 @@ function contentRange (type, size, range) {
|
||||
return type + ' ' + (range ? range.start + '-' + range.end : '*') + '/' + size
|
||||
}
|
||||
|
||||
-/**
|
||||
- * Create a minimal HTML document.
|
||||
- *
|
||||
- * @param {string} title
|
||||
- * @param {string} body
|
||||
- * @private
|
||||
- */
|
||||
-
|
||||
-function createHtmlDocument (title, body) {
|
||||
- return '<!DOCTYPE html>\n' +
|
||||
- '<html lang="en">\n' +
|
||||
- '<head>\n' +
|
||||
- '<meta charset="utf-8">\n' +
|
||||
- '<title>' + title + '</title>\n' +
|
||||
- '</head>\n' +
|
||||
- '<body>\n' +
|
||||
- '<pre>' + body + '</pre>\n' +
|
||||
- '</body>\n' +
|
||||
- '</html>\n'
|
||||
-}
|
||||
-
|
||||
/**
|
||||
* Create a HttpError object from simple arguments.
|
||||
*
|
33
.yarn/plugins/@yarnpkg/plugin-outdated.cjs
vendored
Normal file
33
.yarn/plugins/@yarnpkg/plugin-outdated.cjs
vendored
Normal file
File diff suppressed because one or more lines are too long
28
.yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
vendored
Normal file
28
.yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
vendored
Normal file
File diff suppressed because one or more lines are too long
801
.yarn/releases/yarn-3.2.4.cjs
generated
vendored
Executable file
801
.yarn/releases/yarn-3.2.4.cjs
generated
vendored
Executable file
File diff suppressed because one or more lines are too long
20
.yarnrc.yml
Normal file
20
.yarnrc.yml
Normal file
@ -0,0 +1,20 @@
|
||||
enableInlineHunks: true
|
||||
|
||||
enableMessageNames: false
|
||||
|
||||
initScope: peacockproject
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
packageExtensions:
|
||||
express@*:
|
||||
dependencies:
|
||||
mime: 1.6.0
|
||||
|
||||
plugins:
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-outdated.cjs
|
||||
spec: "https://mskelton.dev/yarn-outdated/v2"
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
|
||||
spec: "@yarnpkg/plugin-workspace-tools"
|
||||
|
||||
yarnPath: .yarn/releases/yarn-3.2.4.cjs
|
661
LICENSE
Normal file
661
LICENSE
Normal file
@ -0,0 +1,661 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
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/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
BIN
PeacockPatcher.exe
Normal file
BIN
PeacockPatcher.exe
Normal file
Binary file not shown.
3
Start Server.cmd
Normal file
3
Start Server.cmd
Normal file
@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
.\nodedist\node.exe chunk0.js
|
||||
PAUSE
|
3263
THIRDPARTYNOTICES.txt
Normal file
3263
THIRDPARTYNOTICES.txt
Normal file
File diff suppressed because it is too large
Load Diff
3
Tools.cmd
Normal file
3
Tools.cmd
Normal file
@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
.\nodedist\node.exe chunk0.js tools
|
||||
PAUSE
|
166
components/2016/legacyContractHandler.ts
Normal file
166
components/2016/legacyContractHandler.ts
Normal file
@ -0,0 +1,166 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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 { Router } from "express"
|
||||
import { gameDifficulty, nilUuid, ServerVer, uuidRegex } from "../utils"
|
||||
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 type { GameChanger, RequestWithJwt } from "../types/types"
|
||||
import { randomUUID } from "crypto"
|
||||
|
||||
const legacyContractRouter = Router()
|
||||
|
||||
legacyContractRouter.post(
|
||||
"/GetForPlay",
|
||||
jsonMiddleware(),
|
||||
(req: RequestWithJwt, res) => {
|
||||
if (!uuidRegex.test(req.body.id)) {
|
||||
res.status(400).end()
|
||||
return
|
||||
}
|
||||
|
||||
const contractData =
|
||||
req.gameVersion === "h1" &&
|
||||
req.body.id === "42bac555-bbb9-429d-a8ce-f1ffdf94211c"
|
||||
? _legacyBull
|
||||
: req.body.id === "ff9f46cf-00bd-4c12-b887-eac491c3a96d"
|
||||
? _theLastYardbirdScpc
|
||||
: controller.resolveContract(req.body.id)
|
||||
|
||||
if (!contractData) {
|
||||
log(
|
||||
LogLevel.ERROR,
|
||||
`Requested unknown contract in LGFP: ${req.body.id}`,
|
||||
)
|
||||
res.status(400).send("no such contract")
|
||||
return
|
||||
}
|
||||
|
||||
if (!contractData.Data.GameChangers) {
|
||||
contractData.Data.GameChangers = []
|
||||
}
|
||||
|
||||
for (const gamechangerId of req.body.extraGameChangerIds) {
|
||||
contractData.Data.GameChangers.push(gamechangerId)
|
||||
}
|
||||
|
||||
if (contractData.Data.GameChangers.length > 0) {
|
||||
type GCPConfig = Record<string, GameChanger>
|
||||
|
||||
const gameChangerData: GCPConfig = {
|
||||
...getConfig<GCPConfig>("GameChangerProperties", true),
|
||||
...getConfig<GCPConfig>("PeacockGameChangerProperties", true),
|
||||
}
|
||||
|
||||
contractData.Data.GameChangerReferences = []
|
||||
|
||||
for (const gameChangerId of contractData.Data.GameChangers) {
|
||||
const gameChanger = gameChangerData[gameChangerId]
|
||||
|
||||
if (!gameChanger) {
|
||||
log(
|
||||
LogLevel.ERROR,
|
||||
`Encountered unknown GameChanger id: ${gameChangerId}`,
|
||||
)
|
||||
res.status(500)
|
||||
continue
|
||||
}
|
||||
|
||||
gameChanger.Id = gameChangerId
|
||||
delete gameChanger.ObjectivesCategory
|
||||
|
||||
contractData.Data.GameChangerReferences.push(gameChanger)
|
||||
|
||||
if (gameChanger.Resource) {
|
||||
contractData.Data.Bricks.push(...gameChanger.Resource)
|
||||
}
|
||||
|
||||
if (gameChanger.Objectives) {
|
||||
contractData.Data.Objectives?.push(
|
||||
...gameChanger.Objectives,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.json(contractData)
|
||||
},
|
||||
)
|
||||
|
||||
legacyContractRouter.post(
|
||||
"/Start",
|
||||
jsonMiddleware(),
|
||||
(req: RequestWithJwt, res) => {
|
||||
if (req.body.profileId !== req.jwt.unique_name) {
|
||||
res.status(400).end() // requested for different user id
|
||||
return
|
||||
}
|
||||
|
||||
if (!uuidRegex.test(req.body.contractId)) {
|
||||
res.status(400).end()
|
||||
return
|
||||
}
|
||||
|
||||
const c = controller.resolveContract(req.body.contractId)
|
||||
|
||||
if (!c) {
|
||||
res.status(404).end()
|
||||
return
|
||||
}
|
||||
|
||||
const contractSessionId = `${process.hrtime
|
||||
.bigint()
|
||||
.toString()}-${randomUUID()}`
|
||||
|
||||
// all event stuff is handled in h3 event handler
|
||||
enqueueEvent(req.jwt.unique_name, {
|
||||
Version: ServerVer,
|
||||
IsReplicated: false,
|
||||
CreatedContract: null,
|
||||
Id: randomUUID(),
|
||||
Name: "ContractSessionMarker",
|
||||
UserId: nilUuid,
|
||||
ContractId: nilUuid,
|
||||
SessionId: null,
|
||||
ContractSessionId: contractSessionId,
|
||||
Timestamp: 0.0,
|
||||
Value: {
|
||||
Currency: {
|
||||
ContractPaymentAllowed: true,
|
||||
ContractPayment: null,
|
||||
},
|
||||
},
|
||||
Origin: null,
|
||||
})
|
||||
|
||||
res.json(contractSessionId)
|
||||
|
||||
newSession(
|
||||
contractSessionId,
|
||||
req.body.contractId,
|
||||
req.jwt.unique_name,
|
||||
gameDifficulty.normal,
|
||||
req.gameVersion,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export { legacyContractRouter }
|
41
components/2016/legacyEventRouter.ts
Normal file
41
components/2016/legacyEventRouter.ts
Normal file
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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 { Router } from "express"
|
||||
import { json as jsonMiddleware } from "body-parser"
|
||||
|
||||
const legacyEventRouter = Router()
|
||||
|
||||
legacyEventRouter.post(
|
||||
"/SaveAndSynchronizeEvents3",
|
||||
jsonMiddleware({ limit: "10Mb" }),
|
||||
(req, res, next) => {
|
||||
// call /SaveAndSynchronizeEvents4 but add/remove dummy pushMessages
|
||||
req.url = "/SaveAndSynchronizeEvents4"
|
||||
req.body.lastPushDt = "0"
|
||||
|
||||
const originalJsonFunc = res.json
|
||||
res.json = function (originalData) {
|
||||
delete originalData.PushMessages
|
||||
return originalJsonFunc.call(this, originalData)
|
||||
}
|
||||
next()
|
||||
},
|
||||
)
|
||||
|
||||
export { legacyEventRouter }
|
247
components/2016/legacyMenuData.ts
Normal file
247
components/2016/legacyMenuData.ts
Normal file
@ -0,0 +1,247 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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 { Router } from "express"
|
||||
import { RequestWithJwt } from "../types/types"
|
||||
import { getConfig } from "../configSwizzleManager"
|
||||
import { getDefaultSuitFor, uuidRegex } from "../utils"
|
||||
import { json as jsonMiddleware } from "body-parser"
|
||||
import { controller } from "../controller"
|
||||
import { generateUserCentric, getSubLocationByName } from "../contracts/dataGen"
|
||||
import { getUserData } from "../databaseHandler"
|
||||
import { log, LogLevel } from "../loggingInterop"
|
||||
import { createInventory } from "../inventory"
|
||||
import { getFlag } from "../flags"
|
||||
import { loadouts } from "../loadouts"
|
||||
|
||||
const legacyMenuDataRouter = Router()
|
||||
|
||||
legacyMenuDataRouter.get(
|
||||
"/stashpoint",
|
||||
(req: RequestWithJwt<{ contractid: string; slotname: string }>, res) => {
|
||||
// stashpoint?contractid=4e45e91a-94ca-4d89-89fc-1b250e608e73&stashpoint=&allowlargeitems=true&slotname=concealedweapon2
|
||||
if (!uuidRegex.test(req.query.contractid)) {
|
||||
res.status(400).send("contract id was not a uuid")
|
||||
return
|
||||
}
|
||||
|
||||
const contractData = controller.resolveContract(req.query.contractid)
|
||||
if (!contractData) {
|
||||
res.status(404).send()
|
||||
return
|
||||
}
|
||||
|
||||
const loadoutSlots = [
|
||||
"carriedweapon",
|
||||
"carrieditem",
|
||||
"concealedweapon",
|
||||
"disguise",
|
||||
"gear",
|
||||
"gear",
|
||||
"stashpoint",
|
||||
]
|
||||
|
||||
if (loadoutSlots.includes(req.query.slotname.slice(0, -1))) {
|
||||
req.query.slotid = req.query.slotname.slice(0, -1)
|
||||
} else {
|
||||
log(
|
||||
LogLevel.ERROR,
|
||||
`Unknown slotname in legacy stashpoint: ${req.query.slotname}`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const userProfile = getUserData(req.jwt.unique_name, req.gameVersion)
|
||||
|
||||
const inventory = createInventory(
|
||||
req.jwt.unique_name,
|
||||
req.gameVersion,
|
||||
userProfile.Extensions.entP,
|
||||
)
|
||||
|
||||
const userCentricContract = generateUserCentric(
|
||||
contractData,
|
||||
req.jwt.unique_name,
|
||||
"h1",
|
||||
)
|
||||
|
||||
const sublocation = getSubLocationByName(
|
||||
contractData.Metadata.Location,
|
||||
req.gameVersion,
|
||||
)
|
||||
|
||||
const defaultLoadout = {
|
||||
2: "FIREARMS_HERO_PISTOL_TACTICAL_001_SU_SKIN01",
|
||||
3: getDefaultSuitFor(sublocation?.Properties?.ParentLocation),
|
||||
4: "TOKEN_FIBERWIRE",
|
||||
5: "PROP_TOOL_COIN",
|
||||
}
|
||||
|
||||
const getLoadoutItem = (id: number) => {
|
||||
if (getFlag("loadoutSaving") === "LEGACY") {
|
||||
const dl = userProfile.Extensions.defaultloadout
|
||||
|
||||
if (!dl) {
|
||||
return defaultLoadout[id]
|
||||
}
|
||||
|
||||
const forLocation = (userProfile.Extensions.defaultloadout ||
|
||||
{})[sublocation?.Properties?.ParentLocation]
|
||||
|
||||
if (!forLocation) {
|
||||
return defaultLoadout[id]
|
||||
}
|
||||
|
||||
return forLocation[id]
|
||||
} else {
|
||||
let dl = loadouts.getLoadoutFor("h1")
|
||||
|
||||
if (!dl) {
|
||||
dl = loadouts.createDefault("h1")
|
||||
}
|
||||
|
||||
const forLocation =
|
||||
dl.data[sublocation?.Properties?.ParentLocation]
|
||||
|
||||
if (!forLocation) {
|
||||
return defaultLoadout[id]
|
||||
}
|
||||
|
||||
return forLocation[id]
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
template: getConfig("LegacyStashpointTemplate", false),
|
||||
data: {
|
||||
ContractId: req.query.contractid,
|
||||
// the game actually only needs the loadoutdata from the requested slotid, but this is what IOI servers do
|
||||
LoadoutData: [...loadoutSlots.entries()].map(
|
||||
([slotid, slotname]) => ({
|
||||
SlotName: slotname,
|
||||
SlotId: slotid.toString(),
|
||||
Items: inventory
|
||||
.filter((item) => {
|
||||
return (
|
||||
item.Unlockable.Properties.LoadoutSlot && // only display items
|
||||
(item.Unlockable.Properties.LoadoutSlot ===
|
||||
slotname || // display items for requested slot
|
||||
(slotname === "stashpoint" && // else: if stashpoint
|
||||
item.Unlockable.Properties
|
||||
.LoadoutSlot !== "disguise")) && // => display all non-disguise items
|
||||
(req.query.allowlargeitems === "true" ||
|
||||
item.Unlockable.Properties
|
||||
.LoadoutSlot !== "carriedweapon")
|
||||
) // not sure about this one
|
||||
})
|
||||
.map((item) => ({
|
||||
Item: item,
|
||||
ItemDetails: {
|
||||
Capabilities: [],
|
||||
StatList: item.Unlockable.Properties
|
||||
.Gameplay
|
||||
? Object.entries(
|
||||
item.Unlockable.Properties
|
||||
.Gameplay,
|
||||
).map(([key, value]) => ({
|
||||
Name: key,
|
||||
Ratio: value,
|
||||
}))
|
||||
: [],
|
||||
PropertyTexts: [],
|
||||
},
|
||||
SlotId: slotid.toString(),
|
||||
SlotName: slotname,
|
||||
})),
|
||||
Page: 0,
|
||||
Recommended: getLoadoutItem(slotid)
|
||||
? {
|
||||
item: inventory.find(
|
||||
(item) =>
|
||||
item.Unlockable.Id ===
|
||||
getLoadoutItem(slotid),
|
||||
),
|
||||
type: loadoutSlots[slotid],
|
||||
owned: true,
|
||||
}
|
||||
: null,
|
||||
HasMore: false,
|
||||
HasMoreLeft: false,
|
||||
HasMoreRight: false,
|
||||
OptionalData:
|
||||
slotid === 6
|
||||
? {
|
||||
stashpoint: req.query.stashpoint,
|
||||
AllowLargeItems:
|
||||
req.query.allowlargeitems ||
|
||||
!req.query.stashpoint,
|
||||
}
|
||||
: {},
|
||||
}),
|
||||
),
|
||||
Contract: userCentricContract.Contract,
|
||||
ShowSlotName: req.query.slotname,
|
||||
UserCentric: userCentricContract,
|
||||
},
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
legacyMenuDataRouter.get("/Safehouse", (req: RequestWithJwt, res, next) => {
|
||||
const template = getConfig("LegacySafehouseTemplate", false)
|
||||
|
||||
// call /SafehouseCategory but rewrite the result a bit
|
||||
req.url = `/SafehouseCategory?page=0&type=${req.query.type}&subtype=`
|
||||
const originalJsonFunc = res.json
|
||||
res.json = function (originalData) {
|
||||
return originalJsonFunc.call(this, {
|
||||
template,
|
||||
data: {
|
||||
SafehouseData: originalData.data,
|
||||
},
|
||||
})
|
||||
}
|
||||
next()
|
||||
})
|
||||
|
||||
legacyMenuDataRouter.get(
|
||||
"/debriefingchallenges",
|
||||
jsonMiddleware(),
|
||||
(
|
||||
req: RequestWithJwt<{ contractSessionId: string; contractId: string }>,
|
||||
res,
|
||||
) => {
|
||||
// debriefingchallenges?contractSessionId=00000000000000-00000000-0000-0000-0000-000000000001&contractId=dd906289-7c32-427f-b689-98ae645b407f
|
||||
res.json({
|
||||
template: getConfig("LegacyDebriefingChallengesTemplate", false),
|
||||
data: {
|
||||
ChallengeData: {
|
||||
// FIXME: This may not work correctly; I don't know the actual format so I'm assuming challenge tree
|
||||
Children:
|
||||
controller.challengeService.getChallengeTreeForContract(
|
||||
req.query.contractId,
|
||||
req.gameVersion,
|
||||
req.jwt.unique_name,
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
export { legacyMenuDataRouter }
|
57
components/2016/legacyMenuSystem.ts
Normal file
57
components/2016/legacyMenuSystem.ts
Normal file
@ -0,0 +1,57 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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 serveStatic from "serve-static"
|
||||
import { Router } from "express"
|
||||
import { join } from "path"
|
||||
import md5File from "md5-file"
|
||||
import { readFile } from "atomically"
|
||||
import { imageFetchingMiddleware } from "../menus/imageHandler"
|
||||
import { MenuSystemDatabase } from "../menus/menuSystem"
|
||||
|
||||
const legacyMenuSystemRouter = Router()
|
||||
|
||||
// /resources-6-74/
|
||||
|
||||
legacyMenuSystemRouter.get(
|
||||
"/dynamic_resources_pc_release_rpkg",
|
||||
async (req, res) => {
|
||||
const filePath = join(
|
||||
PEACOCK_DEV ? process.cwd() : __dirname,
|
||||
"resources",
|
||||
"dynamic_resources_h1.rpkg",
|
||||
)
|
||||
|
||||
const md5 = await md5File(filePath)
|
||||
|
||||
res.set("Content-Type", "application/octet-stream")
|
||||
res.set("Content-MD5", Buffer.from(md5, "hex").toString("base64"))
|
||||
|
||||
res.send(await readFile(filePath))
|
||||
},
|
||||
)
|
||||
|
||||
legacyMenuSystemRouter.use(MenuSystemDatabase.configMiddleware)
|
||||
|
||||
legacyMenuSystemRouter.use(
|
||||
"/images/",
|
||||
serveStatic("images", { fallthrough: true }),
|
||||
imageFetchingMiddleware,
|
||||
)
|
||||
|
||||
export { legacyMenuSystemRouter }
|
158
components/2016/legacyProfileRouter.ts
Normal file
158
components/2016/legacyProfileRouter.ts
Normal file
@ -0,0 +1,158 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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 type {
|
||||
ChallengeProgressionData,
|
||||
CompiledChallengeIngameData,
|
||||
RequestWithJwt,
|
||||
} from "../types/types"
|
||||
import { log, LogLevel } from "../loggingInterop"
|
||||
import { getConfig } from "../configSwizzleManager"
|
||||
|
||||
import { Router } from "express"
|
||||
import { controller } from "../controller"
|
||||
import { getPlatformEntitlements } from "../platformEntitlements"
|
||||
import { json as jsonMiddleware } from "body-parser"
|
||||
import { uuidRegex } from "../utils"
|
||||
import { menuSystemDatabase } from "../menus/menuSystem"
|
||||
import { compileRuntimeChallenge } from "../candle/challengeHelpers"
|
||||
import { LegacyGetProgressionBody } from "../types/gameSchemas"
|
||||
|
||||
const legacyProfileRouter = Router()
|
||||
|
||||
// /authentication/api/userchannel/
|
||||
|
||||
legacyProfileRouter.post(
|
||||
"/ProfileService/GetPlatformEntitlements",
|
||||
jsonMiddleware(),
|
||||
getPlatformEntitlements,
|
||||
)
|
||||
|
||||
legacyProfileRouter.post(
|
||||
"/AuthenticationService/GetBlobOfflineCacheDatabaseDiff",
|
||||
(req: RequestWithJwt, res) => {
|
||||
const configs = []
|
||||
|
||||
menuSystemDatabase.hooks.getDatabaseDiff.call(configs, req.gameVersion)
|
||||
|
||||
res.json(configs)
|
||||
},
|
||||
)
|
||||
|
||||
legacyProfileRouter.post(
|
||||
"/ChallengesService/GetActiveChallenges",
|
||||
jsonMiddleware(),
|
||||
(req: RequestWithJwt, res) => {
|
||||
if (!uuidRegex.test(req.body.contractId)) {
|
||||
return res.status(404).send("invalid contract")
|
||||
}
|
||||
|
||||
const legacyGlobalChallenges = getConfig<CompiledChallengeIngameData[]>(
|
||||
"LegacyGlobalChallenges",
|
||||
false,
|
||||
)
|
||||
|
||||
const json = controller.resolveContract(req.body.contractId)
|
||||
|
||||
if (!json) {
|
||||
log(
|
||||
LogLevel.ERROR,
|
||||
`Unknown contract in LGAC: ${req.body.contractId}`,
|
||||
)
|
||||
return res.status(404).send("contract not found")
|
||||
}
|
||||
|
||||
if (json.Metadata.Type === "creation") {
|
||||
return res.json([])
|
||||
}
|
||||
|
||||
const challenges: CompiledChallengeIngameData[] = legacyGlobalChallenges
|
||||
|
||||
challenges.push(
|
||||
...Object.values(
|
||||
controller.challengeService.getChallengesForContract(
|
||||
json.Metadata.Id,
|
||||
req.gameVersion,
|
||||
),
|
||||
)
|
||||
.flat()
|
||||
.map(
|
||||
(challengeData) =>
|
||||
compileRuntimeChallenge(
|
||||
challengeData,
|
||||
controller.challengeService.getChallengeProgression(
|
||||
req.jwt.unique_name,
|
||||
challengeData.Id,
|
||||
req.gameVersion,
|
||||
),
|
||||
).Challenge,
|
||||
),
|
||||
)
|
||||
|
||||
res.json(challenges)
|
||||
},
|
||||
)
|
||||
|
||||
legacyProfileRouter.post(
|
||||
"/ChallengesService/GetProgression",
|
||||
jsonMiddleware(),
|
||||
(req: RequestWithJwt<never, LegacyGetProgressionBody>, res) => {
|
||||
const legacyGlobalChallenges = getConfig<CompiledChallengeIngameData[]>(
|
||||
"LegacyGlobalChallenges",
|
||||
false,
|
||||
)
|
||||
|
||||
const challenges: ChallengeProgressionData[] =
|
||||
legacyGlobalChallenges.map((challenge) => ({
|
||||
ChallengeId: challenge.Id,
|
||||
ProfileId: req.jwt.unique_name,
|
||||
Completed: false,
|
||||
State: {},
|
||||
ETag: `W/"datetime'${encodeURIComponent(
|
||||
new Date().toISOString(),
|
||||
)}'"`,
|
||||
CompletedAt: null,
|
||||
MustBeSaved: false,
|
||||
}))
|
||||
|
||||
/*
|
||||
challenges.push(
|
||||
...Object.values(
|
||||
controller.challengeService.getChallengesForContract(
|
||||
req.body.contractId,
|
||||
req.gameVersion,
|
||||
),
|
||||
)
|
||||
.flat()
|
||||
.map((challengeData) =>
|
||||
controller.challengeService.getChallengeProgression(
|
||||
req.jwt.unique_name,
|
||||
challengeData.Id,
|
||||
req.gameVersion,
|
||||
),
|
||||
),
|
||||
)
|
||||
*/
|
||||
// TODO: atampy broke this - please fix
|
||||
// (no contract ID given on this route!!)
|
||||
|
||||
res.json(challenges)
|
||||
},
|
||||
)
|
||||
|
||||
export { legacyProfileRouter }
|
9
components/candle/README.md
Normal file
9
components/candle/README.md
Normal file
@ -0,0 +1,9 @@
|
||||
# Candle
|
||||
|
||||
Lighting the way through the new era of Hitman.
|
||||
|
||||
Candle is the subsystem for handling Challenges, Progression, and XP.
|
||||
|
||||
## Credits
|
||||
|
||||
Thanks to Moo for the name suggestion.
|
129
components/candle/challengeHelpers.ts
Normal file
129
components/candle/challengeHelpers.ts
Normal file
@ -0,0 +1,129 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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 {
|
||||
ChallengeProgressionData,
|
||||
CompiledChallengeRewardData,
|
||||
CompiledChallengeRuntimeData,
|
||||
RegistryChallenge,
|
||||
} from "../types/types"
|
||||
import assert from "assert"
|
||||
|
||||
export function compileScoringChallenge(
|
||||
challenge: RegistryChallenge,
|
||||
): CompiledChallengeRewardData {
|
||||
return {
|
||||
ChallengeId: challenge.Id,
|
||||
ChallengeName: challenge.Name,
|
||||
ChallengeDescription: challenge.Description,
|
||||
ChallengeImageUrl: challenge.ImageName,
|
||||
XPGain: challenge.Rewards?.MasteryXP || 0,
|
||||
}
|
||||
}
|
||||
|
||||
export function compileRuntimeChallenge(
|
||||
challenge: RegistryChallenge,
|
||||
progression: ChallengeProgressionData,
|
||||
): CompiledChallengeRuntimeData {
|
||||
return {
|
||||
// GetActiveChallengesAndProgression
|
||||
Challenge: {
|
||||
Id: challenge.Id,
|
||||
GroupId: challenge.inGroup,
|
||||
Name: challenge.Name,
|
||||
Type: challenge.RuntimeType || "contract",
|
||||
Description: challenge.Description,
|
||||
ImageName: challenge.ImageName,
|
||||
InclusionData: challenge.InclusionData || undefined,
|
||||
Definition: challenge.Definition,
|
||||
Tags: challenge.Tags,
|
||||
Drops: challenge.Drops,
|
||||
LastModified: "2021-01-06T23:00:32.0117635", // this is a lie 👍
|
||||
PlayableSince: null,
|
||||
PlayableUntil: null,
|
||||
Xp: challenge.Rewards.MasteryXP || 0,
|
||||
XpModifier: challenge.XpModifier || {},
|
||||
},
|
||||
Progression: progression,
|
||||
}
|
||||
}
|
||||
|
||||
export enum ChallengeFilterType {
|
||||
None = "None",
|
||||
Contract = "Contract",
|
||||
ParentLocation = "ParentLocation",
|
||||
}
|
||||
|
||||
export type ChallengeFilterOptions =
|
||||
| {
|
||||
type: ChallengeFilterType.None
|
||||
}
|
||||
| {
|
||||
type: ChallengeFilterType.Contract
|
||||
contractId: string
|
||||
locationId: string
|
||||
locationParentId: string
|
||||
}
|
||||
| {
|
||||
type: ChallengeFilterType.ParentLocation
|
||||
locationParentId: string
|
||||
}
|
||||
|
||||
export function filterChallenge(
|
||||
options: ChallengeFilterOptions,
|
||||
challenge: RegistryChallenge,
|
||||
): boolean {
|
||||
switch (options.type) {
|
||||
case ChallengeFilterType.None:
|
||||
return true
|
||||
case ChallengeFilterType.Contract: {
|
||||
assert.ok(options.contractId)
|
||||
assert.ok(options.locationId)
|
||||
assert.ok(options.locationParentId)
|
||||
|
||||
if (!challenge) {
|
||||
return false
|
||||
}
|
||||
|
||||
// is this for the current contract?
|
||||
const isForContract = (
|
||||
challenge.InclusionData?.ContractIds || []
|
||||
).includes(options.contractId)
|
||||
|
||||
// is this a location-wide challenge?
|
||||
const isForLocation = challenge.Type === "location"
|
||||
|
||||
// is this for the current location?
|
||||
const isCurrentLocation =
|
||||
// is this challenge for the current parent location?
|
||||
challenge.ParentLocationId === options.locationParentId &&
|
||||
// and, is this challenge's location the current sub-location
|
||||
// or the parent location? (yup, that can happen)
|
||||
(challenge.LocationId === options.locationId ||
|
||||
challenge.LocationId === options.locationParentId)
|
||||
|
||||
return isForContract || (isForLocation && isCurrentLocation)
|
||||
}
|
||||
case ChallengeFilterType.ParentLocation:
|
||||
assert.ok(options.locationParentId)
|
||||
|
||||
return (
|
||||
(challenge?.ParentLocationId || "") === options.locationParentId
|
||||
)
|
||||
}
|
||||
}
|
935
components/candle/challengeService.ts
Normal file
935
components/candle/challengeService.ts
Normal file
@ -0,0 +1,935 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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 type {
|
||||
ChallengeProgressionData,
|
||||
ClientToServerEvent,
|
||||
CompiledChallengeTreeCategory,
|
||||
CompiledChallengeTreeData,
|
||||
ContractSession,
|
||||
GameVersion,
|
||||
PeacockLocationsData,
|
||||
RegistryChallenge,
|
||||
} from "../types/types"
|
||||
import { getUserData, writeUserData } from "../databaseHandler"
|
||||
|
||||
import { Controller } from "../controller"
|
||||
import {
|
||||
generateCompletionData,
|
||||
generateUserCentric,
|
||||
getSubLocationFromContract,
|
||||
} from "../contracts/dataGen"
|
||||
import { log, LogLevel } from "../loggingInterop"
|
||||
import {
|
||||
parseContextListeners,
|
||||
ParsedContextListenerInfo,
|
||||
} from "../statemachines/contextListeners"
|
||||
import {
|
||||
handleEvent,
|
||||
HandleEventOptions,
|
||||
Timer,
|
||||
} from "@peacockproject/statemachine-parser"
|
||||
import { SavedChallengeGroup } from "../types/challenges"
|
||||
import { fastClone } from "../utils"
|
||||
import {
|
||||
ChallengeFilterOptions,
|
||||
ChallengeFilterType,
|
||||
filterChallenge,
|
||||
} from "./challengeHelpers"
|
||||
import assert from "assert"
|
||||
import { getVersionedConfig } from "../configSwizzleManager"
|
||||
import { SyncHook } from "../hooksImpl"
|
||||
|
||||
/**
|
||||
* The structure for a pending write to a user's challenge progression data.
|
||||
*/
|
||||
type PendingProgressionWrite = {
|
||||
userId: string
|
||||
challengeId: string
|
||||
gameVersion: GameVersion
|
||||
progression: ChallengeProgressionData
|
||||
}
|
||||
|
||||
type ChallengeDefinitionLike = {
|
||||
Context?: Record<string, unknown>
|
||||
}
|
||||
|
||||
type GroupIndexedChallengeLists = {
|
||||
[groupId: string]: RegistryChallenge[]
|
||||
}
|
||||
|
||||
/**
|
||||
* A base class providing challenge registration support.
|
||||
*/
|
||||
export abstract class ChallengeRegistry {
|
||||
protected challenges: Map<string, RegistryChallenge> = new Map()
|
||||
protected groups: Map<string, SavedChallengeGroup> = new Map()
|
||||
protected groupContents: Map<string, Set<string>> = new Map()
|
||||
/**
|
||||
* A map of a challenge ID to a list of challenge IDs that it depends on.
|
||||
*/
|
||||
protected readonly _dependencyTree: Map<string, readonly string[]> =
|
||||
new Map()
|
||||
|
||||
protected constructor(protected readonly controller: Controller) {}
|
||||
|
||||
registerChallenge(challenge: RegistryChallenge, groupId: string): void {
|
||||
challenge.inGroup = groupId
|
||||
this.challenges.set(challenge.Id, challenge)
|
||||
|
||||
if (!this.groupContents.has(groupId)) {
|
||||
this.groupContents.set(groupId, new Set())
|
||||
}
|
||||
|
||||
const set = this.groupContents.get(groupId)!
|
||||
set.add(challenge.Id)
|
||||
this.groupContents.set(groupId, set)
|
||||
|
||||
this.checkHeuristics(challenge)
|
||||
}
|
||||
|
||||
registerGroup(group: SavedChallengeGroup): void {
|
||||
this.groups.set(group.CategoryId, group)
|
||||
}
|
||||
|
||||
getChallengeById(challengeId: string): RegistryChallenge | undefined {
|
||||
return this.challenges.get(challengeId)
|
||||
}
|
||||
|
||||
getGroupById(groupId: string): SavedChallengeGroup | undefined {
|
||||
return this.groups.get(groupId)
|
||||
}
|
||||
|
||||
getDependenciesForChallenge(challengeId: string): readonly string[] {
|
||||
return this._dependencyTree.get(challengeId) || []
|
||||
}
|
||||
|
||||
private checkHeuristics(challenge: RegistryChallenge): void {
|
||||
const ctxListeners = ChallengeRegistry._parseContextListeners(challenge)
|
||||
|
||||
if (ctxListeners.challengeTreeIds.length > 0) {
|
||||
this._dependencyTree.set(
|
||||
challenge.Id,
|
||||
ctxListeners.challengeTreeIds,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a challenge's context listeners into the format used internally.
|
||||
*
|
||||
* @param challenge The challenge.
|
||||
* @returns The context listener details.
|
||||
*/
|
||||
protected static _parseContextListeners(
|
||||
challenge: RegistryChallenge,
|
||||
): ParsedContextListenerInfo {
|
||||
return parseContextListeners(
|
||||
challenge.Definition?.ContextListeners || {},
|
||||
{
|
||||
...(challenge.Definition?.Context || {}),
|
||||
...(challenge.Definition?.Constants || {}),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export class ChallengeService extends ChallengeRegistry {
|
||||
private readonly _challengeContexts: {
|
||||
[sessionId: string]: {
|
||||
[challengeId: string]: {
|
||||
context: unknown
|
||||
state: string
|
||||
timers: Timer[]
|
||||
}
|
||||
}
|
||||
}
|
||||
// we'll use this after writing details to the user's profile
|
||||
private _justCompletedChallengeIds: string[] = []
|
||||
public hooks: {
|
||||
/**
|
||||
* A hook that is called when a challenge is completed.
|
||||
*
|
||||
* Params:
|
||||
* - userId: The user's ID.
|
||||
* - challenge: The challenge.
|
||||
* - gameVersion: The game version.
|
||||
*/
|
||||
onChallengeCompleted: SyncHook<[string, RegistryChallenge, GameVersion]>
|
||||
}
|
||||
|
||||
constructor(controller: Controller) {
|
||||
super(controller)
|
||||
this._challengeContexts = {}
|
||||
this.hooks = {
|
||||
onChallengeCompleted: new SyncHook(),
|
||||
}
|
||||
}
|
||||
|
||||
getBatchChallengeProgression(
|
||||
userId: string,
|
||||
gameVersion: GameVersion,
|
||||
): Record<string, ChallengeProgressionData> {
|
||||
const userData = getUserData(userId, gameVersion)
|
||||
|
||||
userData.Extensions.PeacockChallengeProgression ??= {}
|
||||
|
||||
return userData.Extensions.PeacockChallengeProgression
|
||||
}
|
||||
|
||||
getChallengeProgression(
|
||||
userId: string,
|
||||
challengeId: string,
|
||||
gameVersion: GameVersion,
|
||||
batchedData?: Record<string, ChallengeProgressionData>,
|
||||
): ChallengeProgressionData {
|
||||
const data =
|
||||
batchedData ||
|
||||
this.getBatchChallengeProgression(userId, gameVersion)
|
||||
|
||||
const challenge = this.getChallengeById(challengeId)
|
||||
|
||||
if (this._justCompletedChallengeIds.includes(challengeId)) {
|
||||
return {
|
||||
ChallengeId: challengeId,
|
||||
ProfileId: userId,
|
||||
Completed: true,
|
||||
State: {
|
||||
CurrentState: "Success",
|
||||
},
|
||||
ETag: "",
|
||||
CompletedAt: null,
|
||||
MustBeSaved: true,
|
||||
}
|
||||
}
|
||||
|
||||
// prevent game crash
|
||||
if (data[challengeId]?.Completed) {
|
||||
data[challengeId].State = {
|
||||
CurrentState: "Success",
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
data[challengeId] || {
|
||||
ChallengeId: challengeId,
|
||||
ProfileId: userId,
|
||||
Completed: false,
|
||||
State: (<ChallengeDefinitionLike>challenge?.Definition)
|
||||
?.Context,
|
||||
ETag: "",
|
||||
CompletedAt: null,
|
||||
MustBeSaved: true,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get challenge lists sorted into groups.
|
||||
*
|
||||
* @param locationId The parent location's ID.
|
||||
* @param filter The filter to use.
|
||||
*/
|
||||
getGroupedChallengeLists(
|
||||
locationId: string,
|
||||
filter: ChallengeFilterOptions,
|
||||
): GroupIndexedChallengeLists {
|
||||
let challenges: [string, RegistryChallenge[]][] = []
|
||||
|
||||
for (const groupId of this.groups.keys()) {
|
||||
const groupContents = this.groupContents.get(groupId)
|
||||
|
||||
if (groupContents) {
|
||||
let groupChallenges: RegistryChallenge[] | string[] = [
|
||||
...groupContents,
|
||||
]
|
||||
|
||||
groupChallenges = groupChallenges
|
||||
.map((challengeId) => {
|
||||
const challenge = this.getChallengeById(challengeId)
|
||||
|
||||
// early return if the challenge is falsy
|
||||
if (!challenge) {
|
||||
return challenge
|
||||
}
|
||||
|
||||
return filterChallenge(filter, challenge)
|
||||
? challenge
|
||||
: undefined
|
||||
})
|
||||
.filter(Boolean) as RegistryChallenge[]
|
||||
|
||||
challenges.push([groupId, [...groupChallenges]])
|
||||
}
|
||||
}
|
||||
|
||||
// remove empty groups
|
||||
challenges = challenges.filter(
|
||||
([, challenges]) => challenges.length > 0,
|
||||
)
|
||||
|
||||
return Object.fromEntries(challenges)
|
||||
}
|
||||
|
||||
getChallengesForContract(
|
||||
contractId: string,
|
||||
gameVersion: GameVersion,
|
||||
): GroupIndexedChallengeLists {
|
||||
const contract = this.controller.resolveContract(contractId)
|
||||
|
||||
assert.ok(contract)
|
||||
|
||||
const contractParentLocation = getSubLocationFromContract(
|
||||
contract,
|
||||
gameVersion,
|
||||
)?.Properties.ParentLocation
|
||||
|
||||
assert.ok(contractParentLocation)
|
||||
|
||||
return this.getGroupedChallengeLists(contractParentLocation, {
|
||||
type: ChallengeFilterType.Contract,
|
||||
contractId: contractId,
|
||||
locationId: contract.Metadata.Location,
|
||||
locationParentId: contractParentLocation,
|
||||
})
|
||||
}
|
||||
|
||||
startContract(
|
||||
userId: string,
|
||||
sessionId: string,
|
||||
session: ContractSession,
|
||||
): void {
|
||||
this._challengeContexts[sessionId] = {}
|
||||
const { gameVersion, contractId } = session
|
||||
|
||||
const challengeGroups = this.getChallengesForContract(
|
||||
contractId,
|
||||
session.gameVersion,
|
||||
)
|
||||
const batchChallengeProgression = this.getBatchChallengeProgression(
|
||||
userId,
|
||||
gameVersion,
|
||||
)
|
||||
|
||||
const writeQueue: PendingProgressionWrite[] = []
|
||||
|
||||
for (const group of Object.keys(challengeGroups)) {
|
||||
for (const challenge of challengeGroups[group]) {
|
||||
let progression = batchChallengeProgression[challenge.Id]
|
||||
|
||||
if (!progression) {
|
||||
const ctx = fastClone(
|
||||
(<ChallengeDefinitionLike>challenge.Definition)
|
||||
?.Context,
|
||||
)
|
||||
|
||||
progression = {
|
||||
ChallengeId: challenge.Id,
|
||||
ProfileId: userId,
|
||||
Completed: false,
|
||||
State: ctx,
|
||||
ETag: "",
|
||||
CompletedAt: null,
|
||||
MustBeSaved: true,
|
||||
}
|
||||
|
||||
writeQueue.push({
|
||||
userId,
|
||||
gameVersion,
|
||||
progression,
|
||||
challengeId: challenge.Id,
|
||||
})
|
||||
}
|
||||
|
||||
this._challengeContexts[sessionId][challenge.Id] = {
|
||||
context:
|
||||
fastClone(challenge.Definition?.Context || {}) || {},
|
||||
state: progression.Completed ? "Success" : "Start",
|
||||
timers: [],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.writePendingProgression(writeQueue, userId, gameVersion)
|
||||
}
|
||||
|
||||
onContractEvent(
|
||||
event: ClientToServerEvent,
|
||||
sessionId: string,
|
||||
session: ContractSession,
|
||||
): void {
|
||||
const writeQueue: PendingProgressionWrite[] = []
|
||||
|
||||
for (const challengeId of Object.keys(
|
||||
this._challengeContexts[sessionId],
|
||||
)) {
|
||||
const challenge = this.getChallengeById(challengeId)
|
||||
const data = this._challengeContexts[sessionId][challengeId]
|
||||
|
||||
if (!challenge) {
|
||||
log(LogLevel.WARN, `Challenge ${challengeId} not found`)
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const options: HandleEventOptions = {
|
||||
eventName: event.Name,
|
||||
currentState: data.state,
|
||||
timers: data.timers,
|
||||
timestamp: event.Timestamp,
|
||||
}
|
||||
|
||||
const previousState = data.state
|
||||
|
||||
const result = handleEvent(
|
||||
// @ts-expect-error Needs to be fixed upstream.
|
||||
challenge.Definition,
|
||||
fastClone(data.context),
|
||||
event.Value,
|
||||
options,
|
||||
)
|
||||
|
||||
this._challengeContexts[sessionId][challengeId].state =
|
||||
result.state
|
||||
this._challengeContexts[sessionId][challengeId].context =
|
||||
result.context || challenge.Definition?.Context || {}
|
||||
|
||||
if (previousState !== "Success" && result.state === "Success") {
|
||||
this.hooks.onChallengeCompleted.call(
|
||||
session.userId,
|
||||
challenge,
|
||||
session.gameVersion,
|
||||
)
|
||||
|
||||
this._justCompletedChallengeIds.push(challengeId)
|
||||
|
||||
writeQueue.push({
|
||||
challengeId,
|
||||
gameVersion: session.gameVersion,
|
||||
userId: session.userId,
|
||||
progression: {
|
||||
ChallengeId: challenge.Id,
|
||||
ProfileId: session.userId,
|
||||
Completed: true,
|
||||
State: (
|
||||
challenge.Definition as ChallengeDefinitionLike
|
||||
).Context,
|
||||
ETag: "",
|
||||
CompletedAt: new Date().toISOString(),
|
||||
MustBeSaved: true,
|
||||
},
|
||||
})
|
||||
|
||||
this.checkWaterfallCompletion(
|
||||
writeQueue,
|
||||
session,
|
||||
challenge,
|
||||
)
|
||||
}
|
||||
} catch (e) {
|
||||
log(LogLevel.ERROR, e)
|
||||
}
|
||||
}
|
||||
|
||||
this.writePendingProgression(
|
||||
writeQueue,
|
||||
session.userId,
|
||||
session.gameVersion,
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: Rewrite this function out, as we can just modify the context!
|
||||
writePendingProgression(
|
||||
writeQueue: PendingProgressionWrite[],
|
||||
userId: string,
|
||||
gameVersion: GameVersion,
|
||||
): void {
|
||||
if (writeQueue.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const userData = getUserData(userId, gameVersion)
|
||||
|
||||
userData.Extensions.PeacockChallengeProgression ??= {}
|
||||
|
||||
for (const write of writeQueue) {
|
||||
userData.Extensions.PeacockChallengeProgression[write.challengeId] =
|
||||
write.progression
|
||||
}
|
||||
|
||||
writeUserData(userId, gameVersion)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the challenge tree for a contract.
|
||||
*
|
||||
* @param contractId The ID of the contract.
|
||||
* @param gameVersion The game version requesting the challenges.
|
||||
* @param userId The user requesting the challenges' ID.
|
||||
* @returns The challenge tree.
|
||||
*/
|
||||
getChallengeTreeForContract(
|
||||
contractId: string,
|
||||
gameVersion: GameVersion,
|
||||
userId: string,
|
||||
): CompiledChallengeTreeCategory[] {
|
||||
const contractData = this.controller.resolveContract(contractId)
|
||||
|
||||
if (!contractData) {
|
||||
log(LogLevel.WARN, `Contract ${contractId} not found`)
|
||||
return []
|
||||
}
|
||||
|
||||
const subLocation = getSubLocationFromContract(
|
||||
contractData,
|
||||
gameVersion,
|
||||
)
|
||||
|
||||
if (!subLocation) {
|
||||
log(
|
||||
LogLevel.WARN,
|
||||
`Failed to get location data in CTREE [${contractData.Metadata.Location}]`,
|
||||
)
|
||||
return []
|
||||
}
|
||||
|
||||
const forContract = this.getChallengesForContract(
|
||||
contractId,
|
||||
gameVersion,
|
||||
)
|
||||
|
||||
return Object.entries(forContract).map(
|
||||
([groupId, challenges], index) => {
|
||||
const groupData = this.getGroupById(groupId)
|
||||
const challengeProgressionData = challenges.map(
|
||||
(challengeData) =>
|
||||
this.getChallengeProgression(
|
||||
userId,
|
||||
challengeData.Id,
|
||||
gameVersion,
|
||||
),
|
||||
)
|
||||
|
||||
assert.ok(groupData, `Group ${groupId} not found`)
|
||||
|
||||
const lastGroup = this.getGroupById(
|
||||
Object.keys(forContract)[index - 1],
|
||||
)
|
||||
const nextGroup = this.getGroupById(
|
||||
Object.keys(forContract)[index + 1],
|
||||
)
|
||||
|
||||
return {
|
||||
Name: groupData.Name,
|
||||
Description: groupData.Description,
|
||||
Image: groupData.Image,
|
||||
CategoryId: groupData.CategoryId,
|
||||
Icon: groupData.Icon,
|
||||
CompletedChallengesCount: challengeProgressionData.filter(
|
||||
(progressionData) => progressionData.Completed,
|
||||
).length,
|
||||
ChallengesCount: challenges.length,
|
||||
CompletionData: generateCompletionData(
|
||||
contractData.Metadata.Location,
|
||||
userId,
|
||||
gameVersion,
|
||||
),
|
||||
Location: subLocation,
|
||||
IsLocked: subLocation.Properties.IsLocked || false,
|
||||
ImageLocked: subLocation.Properties.LockedIcon || "",
|
||||
RequiredResources:
|
||||
subLocation.Properties.RequiredResources!,
|
||||
SwitchData: {
|
||||
Data: {
|
||||
Challenges: this.mapSwitchChallenges(
|
||||
challenges,
|
||||
userId,
|
||||
gameVersion,
|
||||
),
|
||||
HasPrevious: index !== 0, // whether we are not at the first group
|
||||
HasNext:
|
||||
index !== Object.keys(forContract).length - 1, // whether we are not at the final group
|
||||
PreviousCategoryIcon:
|
||||
index !== 0 ? lastGroup?.Icon : "",
|
||||
NextCategoryIcon:
|
||||
index !== Object.keys(forContract).length - 1
|
||||
? nextGroup?.Icon
|
||||
: "",
|
||||
CategoryData: {
|
||||
Name: groupData.Name,
|
||||
Image: groupData.Image,
|
||||
Icon: groupData.Icon,
|
||||
ChallengesCount: challenges.length,
|
||||
CompletedChallengesCount:
|
||||
challengeProgressionData.filter(
|
||||
(progressionData) =>
|
||||
progressionData.Completed,
|
||||
).length,
|
||||
},
|
||||
CompletionData: generateCompletionData(
|
||||
contractData.Metadata.Location,
|
||||
userId,
|
||||
gameVersion,
|
||||
),
|
||||
},
|
||||
IsLeaf: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private mapSwitchChallenges(
|
||||
challenges: RegistryChallenge[],
|
||||
userId: string,
|
||||
gameVersion: GameVersion,
|
||||
): CompiledChallengeTreeData[] {
|
||||
return challenges.map((challengeData) => {
|
||||
// Handle challenge dependencies
|
||||
const dependencies = this.getDependenciesForChallenge(
|
||||
challengeData.Id,
|
||||
)
|
||||
const completed: string[] = []
|
||||
const missing: string[] = []
|
||||
|
||||
for (const dependency of dependencies) {
|
||||
if (
|
||||
this.getChallengeProgression(
|
||||
userId,
|
||||
challengeData.Id,
|
||||
gameVersion,
|
||||
).Completed
|
||||
) {
|
||||
completed.push(dependency)
|
||||
continue
|
||||
}
|
||||
|
||||
missing.push(dependency)
|
||||
}
|
||||
|
||||
const compiled = this.compileRegistryChallengeTreeData(
|
||||
challengeData,
|
||||
this.getChallengeProgression(
|
||||
userId,
|
||||
challengeData.Id,
|
||||
gameVersion,
|
||||
),
|
||||
gameVersion,
|
||||
userId,
|
||||
)
|
||||
|
||||
const { challengeCountData } =
|
||||
ChallengeService._parseContextListeners(challengeData)
|
||||
|
||||
if (dependencies.length > 0) {
|
||||
compiled.ChallengeProgress = {
|
||||
count: completed.length,
|
||||
completed,
|
||||
total: dependencies.length,
|
||||
missing: missing.length,
|
||||
all: dependencies,
|
||||
}
|
||||
} else if (challengeCountData.total > 0) {
|
||||
compiled.ChallengeProgress = {
|
||||
count: challengeCountData.count,
|
||||
total: challengeCountData.total,
|
||||
}
|
||||
} else {
|
||||
compiled.ChallengeProgress = null
|
||||
}
|
||||
|
||||
return compiled
|
||||
})
|
||||
}
|
||||
|
||||
getChallengePlanningDataForContract(
|
||||
contractId: string,
|
||||
gameVersion: GameVersion,
|
||||
userId: string,
|
||||
): unknown[] {
|
||||
// TODO: fix return type signature
|
||||
|
||||
const contractData = this.controller.resolveContract(contractId)
|
||||
|
||||
if (!contractData) {
|
||||
log(LogLevel.WARN, `Contract ${contractId} not found`)
|
||||
return []
|
||||
}
|
||||
|
||||
const locationData = getSubLocationFromContract(
|
||||
contractData,
|
||||
gameVersion,
|
||||
)
|
||||
|
||||
if (!locationData) {
|
||||
log(
|
||||
LogLevel.WARN,
|
||||
`Failed to get location data in CSERV [${contractData.Metadata.Location}]`,
|
||||
)
|
||||
return []
|
||||
}
|
||||
|
||||
const forContract = this.getChallengesForContract(
|
||||
contractId,
|
||||
gameVersion,
|
||||
)
|
||||
|
||||
return this.reBatchIntoSwitchedData(forContract, userId, gameVersion)
|
||||
}
|
||||
|
||||
getChallengeDataForDestination(
|
||||
locationParentId: string,
|
||||
gameVersion: GameVersion,
|
||||
userId: string,
|
||||
): unknown[] {
|
||||
// TODO: fix return type signature
|
||||
|
||||
const locationsData = getVersionedConfig<PeacockLocationsData>(
|
||||
"LocationsData",
|
||||
gameVersion,
|
||||
false,
|
||||
)
|
||||
|
||||
const locationData = locationsData.parents[locationParentId]
|
||||
|
||||
if (!locationData) {
|
||||
log(
|
||||
LogLevel.WARN,
|
||||
`Failed to get location data in CSERV [${locationParentId}]`,
|
||||
)
|
||||
return []
|
||||
}
|
||||
|
||||
const forLocation = this.getGroupedChallengeLists(locationParentId, {
|
||||
type: ChallengeFilterType.ParentLocation,
|
||||
locationParentId,
|
||||
})
|
||||
|
||||
return this.reBatchIntoSwitchedData(
|
||||
forLocation,
|
||||
userId,
|
||||
gameVersion,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
private reBatchIntoSwitchedData(
|
||||
challenges: GroupIndexedChallengeLists,
|
||||
userId: string,
|
||||
gameVersion: GameVersion,
|
||||
isDestination = false,
|
||||
) {
|
||||
const entries = Object.entries(challenges)
|
||||
const compiler = isDestination
|
||||
? this.compileRegistryDestinationChallengeData.bind(this)
|
||||
: this.compileRegistryChallengeTreeData.bind(this)
|
||||
|
||||
return entries.map(([groupId, challenges]) => {
|
||||
const groupData = this.getGroupById(groupId)
|
||||
const challengeProgressionData = challenges.map((challengeData) =>
|
||||
this.getChallengeProgression(
|
||||
userId,
|
||||
challengeData.Id,
|
||||
gameVersion,
|
||||
),
|
||||
)
|
||||
|
||||
return {
|
||||
Name: groupData?.Name,
|
||||
ChallengesCount: challenges.length,
|
||||
CompletedChallengesCount: challengeProgressionData.filter(
|
||||
(progressionData) => progressionData.Completed,
|
||||
).length,
|
||||
SwitchData: {
|
||||
Data: {
|
||||
Challenges: challenges.map((challengeData) =>
|
||||
compiler(
|
||||
challengeData,
|
||||
this.getChallengeProgression(
|
||||
userId,
|
||||
challengeData.Id,
|
||||
gameVersion,
|
||||
),
|
||||
gameVersion,
|
||||
userId,
|
||||
),
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
compileRegistryChallengeTreeData(
|
||||
challenge: RegistryChallenge,
|
||||
progression: ChallengeProgressionData,
|
||||
gameVersion: GameVersion,
|
||||
userId: string,
|
||||
): CompiledChallengeTreeData {
|
||||
return {
|
||||
// GetChallengeTreeFor
|
||||
Id: challenge.Id,
|
||||
Name: challenge.Name,
|
||||
ImageName: challenge.ImageName,
|
||||
Description: challenge.Description,
|
||||
Rewards: {
|
||||
MasteryXP: challenge.Rewards.MasteryXP,
|
||||
},
|
||||
Drops: [],
|
||||
Completed: progression.Completed,
|
||||
IsPlayable: challenge.IsPlayable || false,
|
||||
IsLocked: challenge.IsLocked || false,
|
||||
HideProgression: false,
|
||||
CategoryName:
|
||||
this.getGroupById(challenge.inGroup!)?.Name || "NOTFOUND",
|
||||
Icon: challenge.Icon,
|
||||
LocationId: challenge.LocationId,
|
||||
ParentLocationId: challenge.ParentLocationId,
|
||||
Type: challenge.Type || "contract",
|
||||
ChallengeProgress: null,
|
||||
DifficultyLevels: [],
|
||||
CompletionData: generateCompletionData(
|
||||
challenge.ParentLocationId,
|
||||
userId,
|
||||
gameVersion,
|
||||
true,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
compileRegistryDestinationChallengeData(
|
||||
challenge: RegistryChallenge,
|
||||
progression: ChallengeProgressionData,
|
||||
gameVersion: GameVersion,
|
||||
userId: string,
|
||||
): CompiledChallengeTreeData {
|
||||
let contract
|
||||
// TODO: Properly get escalation groups for this
|
||||
if (challenge.Type === "contract") {
|
||||
contract = this.controller.resolveContract(
|
||||
challenge.InclusionData?.ContractIds?.[0] || "",
|
||||
)
|
||||
|
||||
// This is so we can remove unused data and make it more like official - AF
|
||||
contract =
|
||||
contract === undefined
|
||||
? null
|
||||
: {
|
||||
// The null is for escalations as we cannot currently get groups
|
||||
Data: {
|
||||
Bricks: contract.Data.Bricks,
|
||||
DevOnlyBricks: null,
|
||||
GameChangerReferences:
|
||||
contract.Data.GameChangerReferences || [],
|
||||
GameChangers: contract.Data.GameChangers || [],
|
||||
GameDifficulties:
|
||||
contract.Data.GameDifficulties || [],
|
||||
},
|
||||
Metadata: {
|
||||
CreationTimestamp: null,
|
||||
CreatorUserId: contract.Metadata.CreatorUserId,
|
||||
DebriefingVideo:
|
||||
contract.Metadata.DebriefingVideo || "",
|
||||
Description: contract.Metadata.Description,
|
||||
Drops: contract.Metadata.Drops || null,
|
||||
Entitlements:
|
||||
contract.Metadata.Entitlements || [],
|
||||
GroupTitle: contract.Metadata.GroupTitle || "",
|
||||
Id: contract.Metadata.Id,
|
||||
IsPublished:
|
||||
contract.Metadata.IsPublished || true,
|
||||
LastUpdate: null,
|
||||
Location: contract.Metadata.Location,
|
||||
PublicId: contract.Metadata.PublicId || "",
|
||||
ScenePath: contract.Metadata.ScenePath,
|
||||
Subtype: contract.Metadata.Subtype || "",
|
||||
TileImage: contract.Metadata.TileImage,
|
||||
Title: contract.Metadata.Title,
|
||||
Type: contract.Metadata.Type,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...this.compileRegistryChallengeTreeData(
|
||||
challenge,
|
||||
progression,
|
||||
gameVersion,
|
||||
userId,
|
||||
),
|
||||
UserCentricContract:
|
||||
challenge.Type === "contract"
|
||||
? generateUserCentric(contract, userId, gameVersion)
|
||||
: (null as unknown as undefined),
|
||||
}
|
||||
}
|
||||
|
||||
private checkWaterfallCompletion(
|
||||
writeQueue: PendingProgressionWrite[],
|
||||
session: ContractSession,
|
||||
challenge: RegistryChallenge,
|
||||
): void {
|
||||
// find any dependency trees that depend on the challenge
|
||||
for (const depTreeId of this._dependencyTree.keys()) {
|
||||
const allDeps = this._dependencyTree.get(depTreeId)
|
||||
|
||||
assert.ok(allDeps, `No dep tree for ${depTreeId}`)
|
||||
|
||||
// check if the dependency tree is completed
|
||||
const completed = allDeps.every((depId) => {
|
||||
const depProgression = this.getChallengeProgression(
|
||||
session.userId,
|
||||
depId,
|
||||
session.gameVersion,
|
||||
)
|
||||
|
||||
return depProgression?.Completed
|
||||
})
|
||||
|
||||
if (!completed) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (PEACOCK_DEV) {
|
||||
log(
|
||||
LogLevel.DEBUG,
|
||||
`${challenge.Id}'s completion caused all conditions to be met for ${depTreeId}`,
|
||||
)
|
||||
}
|
||||
|
||||
writeQueue.push({
|
||||
challengeId: depTreeId,
|
||||
gameVersion: session.gameVersion,
|
||||
userId: session.userId,
|
||||
progression: {
|
||||
ChallengeId: depTreeId,
|
||||
ProfileId: session.userId,
|
||||
Completed: true,
|
||||
State: {
|
||||
...((challenge?.Definition as ChallengeDefinitionLike)
|
||||
?.Context || {}),
|
||||
CurrentState: "Success",
|
||||
},
|
||||
ETag: "",
|
||||
CompletedAt: new Date().toISOString(),
|
||||
MustBeSaved: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
315
components/configSwizzleManager.ts
Normal file
315
components/configSwizzleManager.ts
Normal file
@ -0,0 +1,315 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
// @ts-nocheck
|
||||
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"
|
||||
import { join } from "path"
|
||||
import { log, LogLevel } from "./loggingInterop"
|
||||
|
||||
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 Destinations from "../static/Destinations.json"
|
||||
import connectionConfigTemplate from "../static/config.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 PersistentBools from "../static/PersistentBools.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 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 ContractSearchPageTemplate from "../static/ContractSearchPageTemplate.json"
|
||||
import ContractSearchResponseTemplate from "../static/ContractSearchResponseTemplate.json"
|
||||
import LegacyDebriefingChallengesTemplate from "../static/LegacyDebriefingChallengesTemplate.json"
|
||||
import MasteryUnlockablesTemplate from "../static/MasteryUnlockablesTemplate.json"
|
||||
import SniperLoadouts from "../static/SniperLoadouts.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 type { GameVersion } from "./types/types"
|
||||
import { fastClone } from "./utils"
|
||||
|
||||
/**
|
||||
* All the configurations. Gets modified before being exported.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
const configs: Record<string, unknown> = {
|
||||
Roadmap,
|
||||
StoreData,
|
||||
FilterData,
|
||||
LocationsData,
|
||||
LeaderboardsViewTemplate,
|
||||
LeaderboardEntriesTemplate,
|
||||
GameChangerProperties,
|
||||
allunlockables,
|
||||
Destinations,
|
||||
config: connectionConfigTemplate,
|
||||
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,
|
||||
LegacyLookupContractByIdTemplate,
|
||||
EiderDashboard,
|
||||
PersistentBools,
|
||||
FrankensteinHubTemplate,
|
||||
H2allunlockables,
|
||||
H2DestinationsData,
|
||||
H2StoreData,
|
||||
H2ContractSearchResponseTemplate,
|
||||
H2LocationsData,
|
||||
H2FilterData,
|
||||
H2DashboardTemplate,
|
||||
FrankensteinMmSpTemplate,
|
||||
FrankensteinMmMpTemplate,
|
||||
FrankensteinPlanningTemplate,
|
||||
FrankensteinScoreOverviewTemplate,
|
||||
Videos,
|
||||
ContractSearchPageTemplate,
|
||||
ContractSearchResponseTemplate,
|
||||
MasteryUnlockablesTemplate,
|
||||
SniperLoadouts,
|
||||
Scpcallunlockables,
|
||||
DiscordRichAssetsForBricks,
|
||||
EscalationCodenames,
|
||||
scoreoverviewtemplate,
|
||||
PeacockGameChangerProperties,
|
||||
MultiplayerPresets,
|
||||
LobbySlimTemplate,
|
||||
}
|
||||
|
||||
Object.keys(configs).forEach((cfg) => {
|
||||
const overridePath = join("overrides", `${cfg}.json`)
|
||||
|
||||
if (existsSync(overridePath)) {
|
||||
log(LogLevel.INFO, `Loaded override config for ${cfg}.`)
|
||||
configs[cfg] = JSON.parse(readFileSync(overridePath).toString())
|
||||
}
|
||||
})
|
||||
|
||||
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<T = unknown>(config: string, 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<T = unknown>(
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an override config.
|
||||
*
|
||||
* @param name The name of the config to override.
|
||||
*/
|
||||
export function swizzle(name: string): void {
|
||||
if (existsSync(join("overrides", `${name}.json`))) {
|
||||
log(
|
||||
LogLevel.ERROR,
|
||||
`That file already exists in overrides/${name}.json - Aborting.`,
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (!Object.prototype.hasOwnProperty.call(configs, name)) {
|
||||
log(LogLevel.ERROR, `No configs have the name ${name} - Aborting.`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (!existsSync("overrides")) {
|
||||
mkdirSync("overrides")
|
||||
}
|
||||
|
||||
writeFileSync(
|
||||
join("overrides", `${name}.json`),
|
||||
JSON.stringify(configs[name]),
|
||||
)
|
||||
|
||||
log(LogLevel.INFO, `Done! Wrote override to overrides/${name}.json`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of swizzleable configurations.
|
||||
*
|
||||
* @returns A list of swizzleable configs.
|
||||
*/
|
||||
export function getSwizzleable(): string[] {
|
||||
return Object.keys(configs)
|
||||
}
|
308
components/contracts/contractRouting.ts
Normal file
308
components/contracts/contractRouting.ts
Normal file
@ -0,0 +1,308 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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 { Router } from "express"
|
||||
import { nilUuid, ServerVer, uuidRegex } from "../utils"
|
||||
import { json as jsonMiddleware } from "body-parser"
|
||||
import {
|
||||
enqueueEvent,
|
||||
getSession,
|
||||
newSession,
|
||||
registerObjectiveListener,
|
||||
} from "../eventHandler"
|
||||
import { controller } from "../controller"
|
||||
import { getConfig } from "../configSwizzleManager"
|
||||
import type {
|
||||
CreateFromParamsBody,
|
||||
GameChanger,
|
||||
MissionManifest,
|
||||
MissionManifestObjective,
|
||||
RequestWithJwt,
|
||||
} from "../types/types"
|
||||
import {
|
||||
contractIdToEscalationGroupId,
|
||||
getPlayEscalationInfo,
|
||||
} from "./escalations/escalationService"
|
||||
import { log, LogLevel } from "../loggingInterop"
|
||||
import { randomUUID } from "crypto"
|
||||
import {
|
||||
createTimeLimit,
|
||||
TargetCreator,
|
||||
} from "../statemachines/contractCreation"
|
||||
import { createSniperLoadouts } from "../menus/sniper"
|
||||
import { GetForPlay2Body } from "../types/gameSchemas"
|
||||
import assert from "assert"
|
||||
|
||||
const contractRoutingRouter = Router()
|
||||
|
||||
contractRoutingRouter.post(
|
||||
"/GetForPlay2",
|
||||
jsonMiddleware(),
|
||||
async (req: RequestWithJwt<never, GetForPlay2Body>, res) => {
|
||||
if (!req.body.id || !uuidRegex.test(req.body.id)) {
|
||||
res.status(400).end()
|
||||
return // user sent some nasty info
|
||||
}
|
||||
|
||||
const contractData = controller.resolveContract(req.body.id)
|
||||
|
||||
if (!contractData) {
|
||||
log(
|
||||
LogLevel.ERROR,
|
||||
`Requested unknown contract in GetForPlay2: ${req.body.id}`,
|
||||
)
|
||||
res.status(404).end()
|
||||
return
|
||||
}
|
||||
|
||||
const sniperloadouts = createSniperLoadouts(contractData)
|
||||
const loadoutData = {
|
||||
CharacterLoadoutData:
|
||||
sniperloadouts.length !== 0 ? sniperloadouts : null,
|
||||
}
|
||||
|
||||
// Add escalation data to Contract data HERE
|
||||
// @ts-expect-error TypeScript going crazy
|
||||
contractData.Metadata = {
|
||||
...contractData.Metadata,
|
||||
...(await getPlayEscalationInfo(
|
||||
contractData.Metadata.Type === "escalation",
|
||||
req.jwt.unique_name,
|
||||
contractIdToEscalationGroupId(req.body.id),
|
||||
req.gameVersion,
|
||||
)),
|
||||
...loadoutData,
|
||||
}
|
||||
|
||||
const contractSesh = {
|
||||
Contract: contractData,
|
||||
ContractSessionId: `${process.hrtime
|
||||
.bigint()
|
||||
.toString()}-${randomUUID()}`,
|
||||
ContractProgressionData: null,
|
||||
}
|
||||
|
||||
if (
|
||||
contractData.Data.GameChangers &&
|
||||
contractData.Data.GameChangers.length > 0
|
||||
) {
|
||||
type GCPConfig = Record<string, GameChanger>
|
||||
|
||||
const gameChangerData: GCPConfig = {
|
||||
...getConfig<GCPConfig>("GameChangerProperties", true),
|
||||
...getConfig<GCPConfig>("PeacockGameChangerProperties", true),
|
||||
}
|
||||
|
||||
contractData.Data.GameChangerReferences =
|
||||
contractData.Data.GameChangerReferences || []
|
||||
|
||||
for (const gameChangerId of contractData.Data.GameChangers) {
|
||||
if (
|
||||
!Object.prototype.hasOwnProperty.call(
|
||||
gameChangerData,
|
||||
gameChangerId,
|
||||
)
|
||||
) {
|
||||
log(
|
||||
LogLevel.ERROR,
|
||||
`GetForPlay has detected a missing GameChanger: ${gameChangerId}! This is a bug.`,
|
||||
)
|
||||
}
|
||||
|
||||
const gameChanger = gameChangerData[gameChangerId]
|
||||
gameChanger.Id = gameChangerId
|
||||
delete gameChanger.ObjectivesCategory
|
||||
|
||||
if (
|
||||
contractData.Data.GameChangerReferences.includes(
|
||||
gameChanger,
|
||||
)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
contractData.Data.GameChangerReferences.push(gameChanger)
|
||||
contractData.Data.Bricks = [
|
||||
...(contractData.Data.Bricks ?? []),
|
||||
...(gameChanger.Resource ?? []),
|
||||
]
|
||||
contractData.Data.Objectives = [
|
||||
...(contractData.Data.Objectives ?? []),
|
||||
...(gameChanger.Objectives ?? []),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
enqueueEvent(req.jwt.unique_name, {
|
||||
Version: ServerVer,
|
||||
IsReplicated: false,
|
||||
CreatedContract: null,
|
||||
Id: randomUUID(),
|
||||
Name: "ContractSessionMarker",
|
||||
UserId: nilUuid,
|
||||
ContractId: nilUuid,
|
||||
SessionId: null,
|
||||
ContractSessionId: contractSesh.ContractSessionId,
|
||||
Timestamp: 0.0,
|
||||
Value: {
|
||||
Currency: {
|
||||
ContractPaymentAllowed: true,
|
||||
ContractPayment: null,
|
||||
},
|
||||
},
|
||||
Origin: null,
|
||||
})
|
||||
|
||||
res.json(contractSesh)
|
||||
newSession(
|
||||
contractSesh.ContractSessionId,
|
||||
contractSesh.Contract.Metadata.Id,
|
||||
req.jwt.unique_name,
|
||||
req.body.difficultyLevel!,
|
||||
req.gameVersion,
|
||||
)
|
||||
|
||||
const theSession = getSession(req.jwt.unique_name)
|
||||
|
||||
assert.ok(theSession, "Session should exist")
|
||||
|
||||
for (const obj of contractData.Data.Objectives || []) {
|
||||
// register the objective as a tracked statemachine
|
||||
registerObjectiveListener(theSession, obj)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
contractRoutingRouter.post(
|
||||
"/CreateFromParams",
|
||||
jsonMiddleware(),
|
||||
async (
|
||||
req: RequestWithJwt<Record<never, never>, CreateFromParamsBody>,
|
||||
res,
|
||||
) => {
|
||||
const gameChangerData = getConfig<Record<string, GameChanger>>(
|
||||
"GameChangerProperties",
|
||||
true,
|
||||
)
|
||||
|
||||
const objectives: MissionManifestObjective[] = []
|
||||
const gamechangers: string[] = []
|
||||
const sessionDetails = getSession(req.jwt.unique_name)
|
||||
|
||||
if (!sessionDetails) {
|
||||
res.status(400).end()
|
||||
log(
|
||||
LogLevel.WARN,
|
||||
`CreateFromParams called without a valid session`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// I'm using Math.ceil here to round the time to the nearest next full second
|
||||
// IOI servers don't do this, but that means that the displayed time in the objective
|
||||
// is not accurate with the actual time limit.
|
||||
// If you change this, also change it in menuData.ts
|
||||
const timeLimit = Math.ceil(
|
||||
(sessionDetails.timerEnd as number) -
|
||||
(sessionDetails.timerStart as number),
|
||||
)
|
||||
|
||||
const contractData = controller.resolveContract(
|
||||
sessionDetails.contractId,
|
||||
)
|
||||
|
||||
if (!contractData) {
|
||||
res.status(400).end()
|
||||
log(
|
||||
LogLevel.ERROR,
|
||||
`No such contract creation contract: ${sessionDetails.contractId}`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
req.body.creationData.Targets.forEach((target) => {
|
||||
if (!target.Selected) {
|
||||
return
|
||||
}
|
||||
|
||||
objectives.push(...new TargetCreator(target).build())
|
||||
})
|
||||
|
||||
req.body.creationData.ContractConditionIds.forEach(
|
||||
(contractConditionId) => {
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(
|
||||
gameChangerData,
|
||||
contractConditionId,
|
||||
)
|
||||
) {
|
||||
gamechangers.push(contractConditionId)
|
||||
} else if (
|
||||
contractConditionId ===
|
||||
"1a596216-381e-4592-9798-26f156973942"
|
||||
) {
|
||||
// Optional time limit
|
||||
objectives.push(createTimeLimit(timeLimit, true))
|
||||
} else if (
|
||||
contractConditionId ===
|
||||
"3d6f9119-7ec8-496f-ab4c-ed9757d976a4"
|
||||
) {
|
||||
// Mandatory time limit
|
||||
objectives.push(createTimeLimit(timeLimit, false))
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const theVersion = `${ServerVer._Major}.${ServerVer._Minor}.${ServerVer._Build}.${ServerVer._Revision}`
|
||||
|
||||
const manifest: MissionManifest = {
|
||||
Data: {
|
||||
Objectives: objectives,
|
||||
GameChangers: gamechangers,
|
||||
Bricks: [],
|
||||
},
|
||||
Metadata: {
|
||||
Title: req.body.creationData.Title,
|
||||
Description: req.body.creationData.Description,
|
||||
Entitlements: contractData.Metadata.Entitlements,
|
||||
ScenePath: contractData.Metadata.ScenePath,
|
||||
Location: contractData.Metadata.Location,
|
||||
IsPublished: true,
|
||||
CreatorUserId: "fadb923c-e6bb-4283-a537-eb4d1150262e",
|
||||
GameVersion: theVersion,
|
||||
ServerVersion: theVersion,
|
||||
Type: "usercreated",
|
||||
Id: req.body.creationData.ContractId,
|
||||
PublicId: req.body.creationData.ContractPublicId,
|
||||
TileImage: `$($repository ${req.body.creationData.Targets[0]?.RepositoryId}).Image`,
|
||||
GroupObjectiveDisplayOrder: req.body.creationData.Targets.map(
|
||||
(t) => ({
|
||||
Id: t.RepositoryId,
|
||||
}),
|
||||
),
|
||||
CreationTimestamp: new Date().toISOString(),
|
||||
},
|
||||
UserData: {},
|
||||
}
|
||||
|
||||
await controller.commitNewContract(manifest)
|
||||
res.json(manifest)
|
||||
},
|
||||
)
|
||||
|
||||
export { contractRoutingRouter }
|
58
components/contracts/contractsModeRouting.ts
Normal file
58
components/contracts/contractsModeRouting.ts
Normal file
@ -0,0 +1,58 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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 type { RequestWithJwt } from "../types/types"
|
||||
import type { Response } from "express"
|
||||
import { getConfig, getVersionedConfig } from "../configSwizzleManager"
|
||||
import { getUserData } from "../databaseHandler"
|
||||
import { generateUserCentric } from "./dataGen"
|
||||
import { controller } from "../controller"
|
||||
import { createLocationsData } from "../menus/destinations"
|
||||
|
||||
export function contractsModeHome(req: RequestWithJwt, res: Response): void {
|
||||
const contractsHomeTemplate = getConfig("ContractsTemplate", false)
|
||||
|
||||
const userData = getUserData(req.jwt.unique_name, req.gameVersion)
|
||||
|
||||
const contractCreationTutorial = controller.resolveContract(
|
||||
"d7e2607c-6916-48e2-9588-976c7d8998bb",
|
||||
)
|
||||
|
||||
res.json({
|
||||
template: contractsHomeTemplate,
|
||||
data: {
|
||||
CreateContractTutorial: generateUserCentric(
|
||||
contractCreationTutorial!,
|
||||
req.jwt.unique_name,
|
||||
req.gameVersion,
|
||||
),
|
||||
LocationsData: createLocationsData(req.gameVersion, true),
|
||||
FilterData: getVersionedConfig(
|
||||
"FilterData",
|
||||
req.gameVersion,
|
||||
false,
|
||||
),
|
||||
PlayerProfileXpData: {
|
||||
XP: userData.Extensions.progression.PlayerProfileXP.Total,
|
||||
Level: userData.Extensions.progression.PlayerProfileXP
|
||||
.ProfileLevel,
|
||||
MaxLevel: 5000,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
807
components/contracts/dataGen.ts
Normal file
807
components/contracts/dataGen.ts
Normal file
@ -0,0 +1,807 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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 { getConfig, getVersionedConfig } from "../configSwizzleManager"
|
||||
import type {
|
||||
CompletionData,
|
||||
GameChanger,
|
||||
GameVersion,
|
||||
GroupObjectiveDisplayOrderItem,
|
||||
MissionManifest,
|
||||
MissionManifestObjective,
|
||||
PeacockLocationsData,
|
||||
Unlockable,
|
||||
UserCentricContract,
|
||||
} from "../types/types"
|
||||
import { fastClone, nilUuid } from "../utils"
|
||||
import { log, LogLevel } from "../loggingInterop"
|
||||
import { getUserData } from "../databaseHandler"
|
||||
import { controller } from "../controller"
|
||||
import {
|
||||
getLevelCount,
|
||||
getUserEscalationProgress,
|
||||
} from "./escalations/escalationService"
|
||||
import { translateEntitlements } from "../ownership"
|
||||
|
||||
// TODO: In the near future, this file should be cleaned up where possible.
|
||||
|
||||
/**
|
||||
* Get the sub-location from a contract's location field.
|
||||
*
|
||||
* @param contractData The contract data.
|
||||
* @param gameVersion The game version.
|
||||
* @returns The sub-location.
|
||||
*/
|
||||
export function getSubLocationFromContract(
|
||||
contractData: MissionManifest,
|
||||
gameVersion: GameVersion,
|
||||
): Unlockable | undefined {
|
||||
if (
|
||||
gameVersion === "h1" &&
|
||||
contractData.Metadata.Location.includes("LOCATION_ICA_FACILITY")
|
||||
) {
|
||||
contractData.Metadata.Location = "LOCATION_ICA_FACILITY"
|
||||
}
|
||||
|
||||
return getSubLocationByName(contractData.Metadata.Location, gameVersion)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a sub-location by name.
|
||||
*
|
||||
* @param name The sublocation's name (e.g. `LOCATION_NORTHAMERICA_GARTERSNAKE`).
|
||||
* @param gameVersion The game's version.
|
||||
* @returns The sub-location.
|
||||
*/
|
||||
export function getSubLocationByName(
|
||||
name: string,
|
||||
gameVersion: GameVersion,
|
||||
): Unlockable | undefined {
|
||||
const locationsData = getVersionedConfig<PeacockLocationsData>(
|
||||
"LocationsData",
|
||||
gameVersion,
|
||||
false,
|
||||
)
|
||||
|
||||
return fastClone(locationsData.children[name])
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a CompletionData object.
|
||||
*
|
||||
* @param subLocationId The ID of the targeted sub-location.
|
||||
* @param userId The ID of the user.
|
||||
* @param gameVersion The game's version.
|
||||
* @param isParentLocation If this is meant to be focused on a parent location.
|
||||
* If true, the SubLocationId property will not be set.
|
||||
* @returns The completion data object.
|
||||
*/
|
||||
export function generateCompletionData(
|
||||
subLocationId: string,
|
||||
userId: string,
|
||||
gameVersion: GameVersion,
|
||||
isParentLocation = false,
|
||||
): CompletionData {
|
||||
// TODO(v6/v7): fetch actual statistics from the user's profile.
|
||||
|
||||
const subLocation = getSubLocationByName(subLocationId, gameVersion)
|
||||
|
||||
return {
|
||||
Level: 20,
|
||||
MaxLevel: 20,
|
||||
XP: 0,
|
||||
Completion: 1,
|
||||
XpLeft: 6000,
|
||||
Id: isParentLocation
|
||||
? subLocationId
|
||||
: subLocation?.Properties?.ParentLocation,
|
||||
SubLocationId: subLocation?.Id,
|
||||
HideProgression: false,
|
||||
IsLocationProgression: true,
|
||||
Name: null,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes a contract to the "UserCentric" format.
|
||||
*
|
||||
* @param contractData Data about the contract.
|
||||
* @param userId The target user's ID.
|
||||
* @param gameVersion The game version.
|
||||
* @returns The user-centric contract.
|
||||
*/
|
||||
export function generateUserCentric(
|
||||
contractData: MissionManifest | undefined,
|
||||
userId: string,
|
||||
gameVersion: GameVersion,
|
||||
): UserCentricContract | undefined {
|
||||
if (!contractData) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const subLocation = getSubLocationFromContract(contractData, gameVersion)
|
||||
|
||||
if (!subLocation) {
|
||||
log(
|
||||
LogLevel.DEBUG,
|
||||
`Missing ${contractData.Metadata.Location} in ${gameVersion} config!`,
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
|
||||
subLocation.DisplayNameLocKey = `UI_${subLocation!.Id}_NAME`
|
||||
|
||||
if (gameVersion === "h1" || gameVersion === "h2") {
|
||||
// fix h1/h2 entitlements
|
||||
contractData.Metadata.Entitlements = translateEntitlements(
|
||||
gameVersion,
|
||||
contractData.Metadata.Entitlements,
|
||||
)
|
||||
}
|
||||
|
||||
const uc: UserCentricContract = {
|
||||
Contract: contractData,
|
||||
Data: {
|
||||
IsLocked: subLocation?.Properties?.IsLocked || false,
|
||||
LockedReason: "",
|
||||
LocationLevel: 1,
|
||||
LocationMaxLevel: 1,
|
||||
LocationCompletion: 1,
|
||||
LocationXpLeft: 0,
|
||||
LocationHideProgression: false,
|
||||
ElusiveContractState: "",
|
||||
IsFeatured: false,
|
||||
//LastPlayedAt: '2020-01-01T00:00:00.0000000Z', // ISO timestamp
|
||||
Completed: false, // relevant for featured contracts
|
||||
LocationId: subLocation.Id,
|
||||
ParentLocationId: subLocation.Properties.ParentLocation!,
|
||||
CompletionData: generateCompletionData(
|
||||
contractData.Metadata.Location,
|
||||
userId,
|
||||
gameVersion,
|
||||
),
|
||||
DlcName: subLocation.Properties.DlcName!,
|
||||
DlcImage: subLocation.Properties.DlcImage!,
|
||||
},
|
||||
}
|
||||
|
||||
if (contractData.Metadata.Type === "escalation") {
|
||||
const userData = getUserData(userId, gameVersion)
|
||||
|
||||
const eGroupId = contractData.Metadata.InGroup
|
||||
|
||||
if (eGroupId) {
|
||||
const p = getUserEscalationProgress(userData, eGroupId)
|
||||
|
||||
log(
|
||||
LogLevel.DEBUG,
|
||||
`Get EscalationUCProps - group: ${eGroupId} prog: ${p}`,
|
||||
)
|
||||
|
||||
// I have absolutely no idea why,
|
||||
// but this is incorrect on the destinations
|
||||
// screen unless we do proper count - 1
|
||||
// ANOTHER NOTE - Anthony:
|
||||
// this currently doesn't mark it as completed when it is,
|
||||
// unknown to why
|
||||
uc.Data.EscalationCompletedLevels = p - 1
|
||||
uc.Data.EscalationTotalLevels = getLevelCount(
|
||||
controller.escalationMappings[eGroupId],
|
||||
)
|
||||
uc.Data.InGroup = eGroupId
|
||||
}
|
||||
}
|
||||
|
||||
return uc
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a series of objectives into the format that the planning screen expects.
|
||||
*
|
||||
* @param objectives The objectives.
|
||||
* @param gameChangers The game changers.
|
||||
* @param displayOrder The order in which to display the objectives.
|
||||
* @returns The converted objectives.
|
||||
*/
|
||||
export function mapObjectives(
|
||||
objectives: MissionManifestObjective[],
|
||||
gameChangers: string[],
|
||||
displayOrder: GroupObjectiveDisplayOrderItem[],
|
||||
): MissionManifestObjective[] {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const result = new Map<string, any>()
|
||||
const gameChangerObjectives: MissionManifestObjective[] = []
|
||||
|
||||
if (gameChangers && gameChangers.length > 0) {
|
||||
const gameChangerData = getConfig<Record<string, GameChanger>>(
|
||||
"GameChangerProperties",
|
||||
true,
|
||||
)
|
||||
for (const gamechangerId of gameChangers) {
|
||||
const gameChangerProps = gameChangerData[gamechangerId]
|
||||
if (gameChangerProps) {
|
||||
if (gameChangerProps.IsHidden) {
|
||||
if (gameChangerProps.Objectives?.length === 1) {
|
||||
// Either 0 or 1 I think.
|
||||
const objective = gameChangerProps.Objectives[0]!
|
||||
objective.Id = gamechangerId
|
||||
gameChangerObjectives.push(objective)
|
||||
}
|
||||
} else {
|
||||
result.set(gamechangerId, {
|
||||
Type: "gamechanger",
|
||||
Properties: {
|
||||
Id: gamechangerId,
|
||||
Name: gameChangerProps.Name,
|
||||
Description: gameChangerProps.Description,
|
||||
LongDescription:
|
||||
gameChangerProps.LongDescription === undefined
|
||||
? gameChangerProps.Description
|
||||
: gameChangerProps.LongDescription,
|
||||
TileImage: gameChangerProps.TileImage,
|
||||
Icon: gameChangerProps.Icon || "",
|
||||
ObjectivesCategory:
|
||||
gameChangerProps.ObjectivesCategory,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const objective of (objectives || []).concat(gameChangerObjectives)) {
|
||||
if (!objective.Category) {
|
||||
objective.Category = objective.Primary ? "primary" : "secondary"
|
||||
}
|
||||
if (
|
||||
objective.Activation ||
|
||||
(objective.OnActive?.IfInProgress &&
|
||||
objective.OnActive.IfInProgress.Visible === false) ||
|
||||
(objective.OnActive?.IfCompleted &&
|
||||
objective.OnActive.IfCompleted.Visible === false &&
|
||||
objective.Definition &&
|
||||
objective.Definition.States &&
|
||||
objective.Definition.States.Start &&
|
||||
objective.Definition.States.Start["-"] &&
|
||||
objective.Definition.States.Start["-"].Transition === "Success")
|
||||
) {
|
||||
continue // do not show objectives with 'ForceShowOnLoadingScreen: false' or objectives that are not visible on start
|
||||
}
|
||||
|
||||
if (
|
||||
objective.SuccessEvent &&
|
||||
objective.SuccessEvent.EventName === "Kill" &&
|
||||
objective.SuccessEvent.EventValues &&
|
||||
objective.SuccessEvent.EventValues.RepositoryId
|
||||
) {
|
||||
result.set(objective.Id, {
|
||||
Type: "kill",
|
||||
Properties: {
|
||||
Id: objective.SuccessEvent.EventValues.RepositoryId,
|
||||
Conditions: [],
|
||||
},
|
||||
})
|
||||
} else if (
|
||||
objective.HUDTemplate &&
|
||||
["custom", "customkill", "setpiece"].includes(
|
||||
objective.ObjectiveType || "",
|
||||
)
|
||||
) {
|
||||
let id: string | null | undefined = null
|
||||
if (
|
||||
objective.Definition?.Context?.Targets &&
|
||||
(objective.Definition.Context.Targets as string[]).length === 1
|
||||
) {
|
||||
id = objective.Definition.Context.Targets[0]
|
||||
}
|
||||
|
||||
const properties = {
|
||||
Id: id,
|
||||
BriefingText: objective.BriefingText || "",
|
||||
LongBriefingText:
|
||||
objective.LongBriefingText === undefined
|
||||
? objective.BriefingText || ""
|
||||
: objective.LongBriefingText,
|
||||
Image: objective.Image,
|
||||
BriefingName: objective.BriefingName,
|
||||
DisplayAsKill: objective.DisplayAsKillObjective || false,
|
||||
ObjectivesCategory: objective.Category as
|
||||
| typeof objective.Category
|
||||
| undefined,
|
||||
ForceShowOnLoadingScreen: (objective.ForceShowOnLoadingScreen ||
|
||||
false) as boolean | undefined,
|
||||
}
|
||||
|
||||
// noinspection GrazieInspection
|
||||
switch (objective.ObjectiveType) {
|
||||
case "customkill":
|
||||
properties.Image = undefined
|
||||
properties.ForceShowOnLoadingScreen = undefined
|
||||
properties.BriefingName = undefined
|
||||
break
|
||||
case "setpiece":
|
||||
properties.ObjectivesCategory = undefined
|
||||
break
|
||||
default: // only add Id for customkill and setpiece
|
||||
properties.Id = undefined
|
||||
break
|
||||
}
|
||||
|
||||
result.set(objective.Id, {
|
||||
Type: objective.ObjectiveType,
|
||||
Properties: properties,
|
||||
})
|
||||
} else if (
|
||||
objective.Type === "statemachine" &&
|
||||
objective.Definition &&
|
||||
objective.Definition.Context &&
|
||||
objective.Definition.Context.Targets &&
|
||||
(objective.Definition.Context.Targets as unknown[]).length === 1 &&
|
||||
objective.HUDTemplate
|
||||
) {
|
||||
// This objective will be displayed as a kill objective
|
||||
const Conditions = objective.TargetConditions
|
||||
? objective.TargetConditions.map((condition) => ({
|
||||
Type: condition.Type,
|
||||
RepositoryId: condition.RepositoryId || nilUuid,
|
||||
HardCondition: condition.HardCondition || false,
|
||||
ObjectiveId: condition.ObjectiveId || nilUuid,
|
||||
KillMethod: condition.KillMethod || "",
|
||||
}))
|
||||
: []
|
||||
|
||||
result.set(objective.Id, {
|
||||
Type: "kill",
|
||||
Properties: {
|
||||
Id: objective.Definition.Context.Targets[0],
|
||||
Conditions: Conditions,
|
||||
},
|
||||
})
|
||||
}
|
||||
// objective not shown on planning screen
|
||||
}
|
||||
|
||||
const sortedResult: MissionManifestObjective[] = []
|
||||
const resultIds: Set<string> = new Set()
|
||||
for (const { Id, IsNew } of displayOrder || []) {
|
||||
if (!resultIds.has(Id)) {
|
||||
// if not yet added
|
||||
const objective = result.get(Id)
|
||||
if (objective) {
|
||||
if (IsNew) {
|
||||
objective.Properties.IsNew = true
|
||||
}
|
||||
sortedResult.push(objective)
|
||||
resultIds.add(Id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add each objective or gamechanger that is not already in the result
|
||||
for (const { Id, ExcludeFromScoring, ForceShowOnLoadingScreen } of (
|
||||
objectives || []
|
||||
).concat((gameChangers || []).map((x) => ({ Id: x })))) {
|
||||
if (!resultIds.has(Id)) {
|
||||
const resultobjective = result.get(Id)
|
||||
if (
|
||||
resultobjective &&
|
||||
(!ExcludeFromScoring || ForceShowOnLoadingScreen)
|
||||
) {
|
||||
sortedResult.push(resultobjective)
|
||||
resultIds.add(Id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sortedResult
|
||||
}
|
||||
|
||||
const noDisguiseChanges = {
|
||||
IsCompleted: true,
|
||||
ContractConditionType: "PrimarySecondary",
|
||||
Primary: [
|
||||
{
|
||||
Type: "gamechanger",
|
||||
Properties: {
|
||||
Id: "63055f1a-bcd2-4e0f-8caf-b446f01d02f3",
|
||||
Name: "UI_GAMECHANGERS_GLOBAL_CONTRACTCONDITION_NO_DISGUISE_CHANGES_PRIMARY_NAME",
|
||||
Description:
|
||||
"UI_GAMECHANGERS_GLOBAL_CONTRACTCONDITION_NO_DISGUISE_CHANGES_PRIMARY_DESC",
|
||||
LongDescription:
|
||||
"UI_GAMECHANGERS_GLOBAL_CONTRACTCONDITION_NO_DISGUISE_CHANGES_PRIMARY_DESC",
|
||||
TileImage:
|
||||
"images/contractconditions/condition_contract_no_disguise_changes.jpg",
|
||||
Icon: "images/challenges/default_challenge_icon.png",
|
||||
ObjectivesCategory: "primary",
|
||||
},
|
||||
},
|
||||
],
|
||||
Secondary: [
|
||||
{
|
||||
Type: "gamechanger",
|
||||
Properties: {
|
||||
Id: "008d2eb9-c1c8-44e0-a636-ccca63629f3c",
|
||||
Name: "UI_GAMECHANGERS_GLOBAL_CONTRACTCONDITION_NO_DISGUISE_CHANGES_SECONDARY_NAME",
|
||||
Description:
|
||||
"UI_GAMECHANGERS_GLOBAL_CONTRACTCONDITION_NO_DISGUISE_CHANGES_SECONDARY_DESC",
|
||||
LongDescription:
|
||||
"UI_GAMECHANGERS_GLOBAL_CONTRACTCONDITION_NO_DISGUISE_CHANGES_SECONDARY_DESC",
|
||||
TileImage:
|
||||
"images/contractconditions/condition_contract_no_disguise_changes.jpg",
|
||||
Icon: "images/challenges/default_challenge_icon.png",
|
||||
ObjectivesCategory: "secondary",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const targetsOnly = {
|
||||
IsCompleted: true,
|
||||
ContractConditionType: "PrimarySecondary",
|
||||
Primary: [
|
||||
{
|
||||
Type: "gamechanger",
|
||||
Properties: {
|
||||
Id: "f41f18fe-0fe5-416a-a793-50727e594655",
|
||||
Name: "UI_GAMECHANGERS_GLOBAL_CONTRACTCONDITION_TARGETS_ONLY_PRIMARY_NAME",
|
||||
Description:
|
||||
"UI_GAMECHANGERS_GLOBAL_CONTRACTCONDITION_TARGETS_ONLY_PRIMARY_DESC",
|
||||
LongDescription:
|
||||
"UI_GAMECHANGERS_GLOBAL_CONTRACTCONDITION_TARGETS_ONLY_PRIMARY_DESC",
|
||||
TileImage:
|
||||
"images/contractconditions/condition_contrac_targets_only.jpg",
|
||||
Icon: "images/challenges/default_challenge_icon.png",
|
||||
ObjectivesCategory: "primary",
|
||||
},
|
||||
},
|
||||
],
|
||||
Secondary: [
|
||||
{
|
||||
Type: "gamechanger",
|
||||
Properties: {
|
||||
Id: "8618ebaa-f42b-42ce-be20-00d2b0a04897",
|
||||
Name: "UI_GAMECHANGERS_GLOBAL_CONTRACTCONDITION_TARGETS_ONLY_SECONDARY_NAME",
|
||||
Description:
|
||||
"UI_GAMECHANGERS_GLOBAL_CONTRACTCONDITION_TARGETS_ONLY_SECONDARY_DESC",
|
||||
LongDescription:
|
||||
"UI_GAMECHANGERS_GLOBAL_CONTRACTCONDITION_TARGETS_ONLY_SECONDARY_DESC",
|
||||
TileImage:
|
||||
"images/contractconditions/condition_contrac_targets_only.jpg",
|
||||
Icon: "images/challenges/default_challenge_icon.png",
|
||||
ObjectivesCategory: "secondary",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const noRecordings = {
|
||||
IsCompleted: true,
|
||||
ContractConditionType: "PrimarySecondary",
|
||||
Primary: [
|
||||
{
|
||||
Type: "gamechanger",
|
||||
Properties: {
|
||||
Id: "1f1f3c9e-1490-4fcc-aee6-5fde7c6c48ca",
|
||||
Name: "UI_GAMECHANGERS_GLOBAL_CONTRACTCONDITION_NO_RECORDINGS_PRIMARY_NAME",
|
||||
Description:
|
||||
"UI_GAMECHANGERS_GLOBAL_CONTRACTCONDITION_NO_RECORDINGS_PRIMARY_DESC",
|
||||
LongDescription:
|
||||
"UI_GAMECHANGERS_GLOBAL_CONTRACTCONDITION_NO_RECORDINGS_PRIMARY_DESC",
|
||||
TileImage:
|
||||
"images/contracts/gamechangers/gamechanger_global_bigbrother.jpg",
|
||||
Icon: "images/challenges/default_challenge_icon.png",
|
||||
ObjectivesCategory: "primary",
|
||||
},
|
||||
},
|
||||
],
|
||||
Secondary: [
|
||||
{
|
||||
Type: "gamechanger",
|
||||
Properties: {
|
||||
Id: "1f8f0b8b-1f65-4d6c-a2f4-fc8adffa394a",
|
||||
Name: "UI_GAMECHANGERS_GLOBAL_CONTRACTCONDITION_NO_RECORDINGS_SECONDARY_NAME",
|
||||
Description:
|
||||
"UI_GAMECHANGERS_GLOBAL_CONTRACTCONDITION_NO_RECORDINGS_SECONDARY_DESC",
|
||||
LongDescription:
|
||||
"UI_GAMECHANGERS_GLOBAL_CONTRACTCONDITION_NO_RECORDINGS_SECONDARY_DESC",
|
||||
TileImage:
|
||||
"images/contracts/gamechangers/gamechanger_global_bigbrother.jpg",
|
||||
Icon: "images/challenges/default_challenge_icon.png",
|
||||
ObjectivesCategory: "secondary",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const headshotsOnly = {
|
||||
IsCompleted: true,
|
||||
ContractConditionType: "PrimarySecondary",
|
||||
Primary: [
|
||||
{
|
||||
Type: "gamechanger",
|
||||
Properties: {
|
||||
Id: "3fea3aea-0233-46bb-8bc1-08757a2f6a74",
|
||||
Name: "UI_GAMECHANGERS_GLOBAL_CONTRACTCONDITION_HEADSHOTS_ONLY_PRIMARY_NAME",
|
||||
Description:
|
||||
"UI_GAMECHANGERS_GLOBAL_CONTRACTCONDITION_HEADSHOTS_ONLY_PRIMARY_DESC",
|
||||
LongDescription:
|
||||
"UI_GAMECHANGERS_GLOBAL_CONTRACTCONDITION_HEADSHOTS_ONLY_PRIMARY_DESC",
|
||||
TileImage:
|
||||
"images/contractconditions/condition_contrac_headshots_only.jpg",
|
||||
Icon: "images/challenges/default_challenge_icon.png",
|
||||
ObjectivesCategory: "primary",
|
||||
},
|
||||
},
|
||||
],
|
||||
Secondary: [
|
||||
{
|
||||
Type: "gamechanger",
|
||||
Properties: {
|
||||
Id: "1efef5c0-7381-4e22-ac04-ffbd0822cc96",
|
||||
Name: "UI_GAMECHANGERS_GLOBAL_CONTRACTCONDITION_HEADSHOTS_ONLY_SECONDARY_NAME",
|
||||
Description:
|
||||
"UI_GAMECHANGERS_GLOBAL_CONTRACTCONDITION_HEADSHOTS_ONLY_SECONDARY_DESC",
|
||||
LongDescription:
|
||||
"UI_GAMECHANGERS_GLOBAL_CONTRACTCONDITION_HEADSHOTS_ONLY_SECONDARY_DESC",
|
||||
TileImage:
|
||||
"images/contractconditions/condition_contrac_headshots_only.jpg",
|
||||
Icon: "images/challenges/default_challenge_icon.png",
|
||||
ObjectivesCategory: "secondary",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const noMissedShots = {
|
||||
IsCompleted: true,
|
||||
ContractConditionType: "PrimarySecondary",
|
||||
Primary: [
|
||||
{
|
||||
Type: "gamechanger",
|
||||
Properties: {
|
||||
Id: "25760ea6-958b-4aab-97d4-b539c5b025c8",
|
||||
Name: "UI_GAMECHANGERS_GLOBAL_CONTRACTCONDITION_NO_MISSED_SHOTS_PRIMARY_NAME",
|
||||
Description:
|
||||
"UI_GAMECHANGERS_GLOBAL_CONTRACTCONDITION_NO_MISSED_SHOTS_PRIMARY_DESC",
|
||||
LongDescription:
|
||||
"UI_GAMECHANGERS_GLOBAL_CONTRACTCONDITION_NO_MISSED_SHOTS_PRIMARY_DESC",
|
||||
TileImage:
|
||||
"images/contractconditions/condition_contrac_no_missed_shots.jpg",
|
||||
Icon: "images/challenges/default_challenge_icon.png",
|
||||
ObjectivesCategory: "primary",
|
||||
},
|
||||
},
|
||||
],
|
||||
Secondary: [
|
||||
{
|
||||
Type: "gamechanger",
|
||||
Properties: {
|
||||
Id: "f96e94b7-1c0e-49c9-9332-07346a955fd2",
|
||||
Name: "UI_GAMECHANGERS_GLOBAL_CONTRACTCONDITION_NO_MISSED_SHOTS_SECONDARY_NAME",
|
||||
Description:
|
||||
"UI_GAMECHANGERS_GLOBAL_CONTRACTCONDITION_NO_MISSED_SHOTS_SECONDARY_DESC",
|
||||
LongDescription:
|
||||
"UI_GAMECHANGERS_GLOBAL_CONTRACTCONDITION_NO_MISSED_SHOTS_SECONDARY_DESC",
|
||||
TileImage:
|
||||
"images/contractconditions/condition_contrac_no_missed_shots.jpg",
|
||||
Icon: "images/challenges/default_challenge_icon.png",
|
||||
ObjectivesCategory: "secondary",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const noPacifications = {
|
||||
IsCompleted: true,
|
||||
ContractConditionType: "PrimarySecondary",
|
||||
Primary: [
|
||||
{
|
||||
Type: "gamechanger",
|
||||
Properties: {
|
||||
Id: "ce154566-a4ba-43c5-be4e-79240ce0f3f9",
|
||||
Name: "UI_GAMECHANGERS_GLOBAL_CONTRACTCONDITION_NO_PACIFICATIONS_PRIMARY_NAME",
|
||||
Description:
|
||||
"UI_GAMECHANGERS_GLOBAL_CONTRACTCONDITION_NO_PACIFICATIONS_PRIMARY_DESC",
|
||||
LongDescription:
|
||||
"UI_GAMECHANGERS_GLOBAL_CONTRACTCONDITION_NO_PACIFICATIONS_PRIMARY_DESC",
|
||||
TileImage:
|
||||
"images/contracts/gamechangers/Gamechanger_Global_NoPacifications.jpg",
|
||||
Icon: "images/challenges/default_challenge_icon.png",
|
||||
ObjectivesCategory: "primary",
|
||||
},
|
||||
},
|
||||
],
|
||||
Secondary: [
|
||||
{
|
||||
Type: "gamechanger",
|
||||
Properties: {
|
||||
Id: "95690829-7da4-4225-a087-08918cccf120",
|
||||
Name: "UI_GAMECHANGERS_GLOBAL_CONTRACTCONDITION_NO_PACIFICATIONS_SECONDARY_NAME",
|
||||
Description:
|
||||
"UI_GAMECHANGERS_GLOBAL_CONTRACTCONDITION_NO_PACIFICATIONS_SECONDARY_DESC",
|
||||
LongDescription:
|
||||
"UI_GAMECHANGERS_GLOBAL_CONTRACTCONDITION_NO_PACIFICATIONS_SECONDARY_DESC",
|
||||
TileImage:
|
||||
"images/contracts/gamechangers/Gamechanger_Global_NoPacifications.jpg",
|
||||
Icon: "images/challenges/default_challenge_icon.png",
|
||||
ObjectivesCategory: "secondary",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export function complications(timeString: string) {
|
||||
return [
|
||||
{
|
||||
IsCompleted: true,
|
||||
ContractConditionType: "PrimarySecondary",
|
||||
Primary: [
|
||||
{
|
||||
Type: "custom",
|
||||
Properties: {
|
||||
Id: "3d6f9119-7ec8-496f-ab4c-ed9757d976a4",
|
||||
BriefingName: "$loc UI_CONTRACT_UGC_TIME_LIMIT_NAME",
|
||||
BriefingText: {
|
||||
$loc: {
|
||||
key: "UI_CONTRACT_UGC_TIME_LIMIT_PRIMARY_DESC",
|
||||
data: `$formatstring ${timeString}`,
|
||||
},
|
||||
},
|
||||
LongBriefingText: {
|
||||
$loc: {
|
||||
key: "UI_CONTRACT_UGC_TIME_LIMIT_PRIMARY_DESC",
|
||||
data: `$formatstring ${timeString}`,
|
||||
},
|
||||
},
|
||||
Image: "images/contractconditions/condition_contrac_time_limit.jpg",
|
||||
ObjectivesCategory: "primary",
|
||||
},
|
||||
},
|
||||
],
|
||||
Secondary: [
|
||||
{
|
||||
Type: "custom",
|
||||
Properties: {
|
||||
Id: "1a596216-381e-4592-9798-26f156973942",
|
||||
BriefingName: "$loc UI_CONTRACT_UGC_TIME_LIMIT_NAME",
|
||||
BriefingText: {
|
||||
$loc: {
|
||||
key: "UI_CONTRACT_UGC_TIME_LIMIT_SECONDARY_DESC",
|
||||
data: `$formatstring ${timeString}`,
|
||||
},
|
||||
},
|
||||
LongBriefingText: {
|
||||
$loc: {
|
||||
key: "UI_CONTRACT_UGC_TIME_LIMIT_SECONDARY_DESC",
|
||||
data: `$formatstring ${timeString}`,
|
||||
},
|
||||
},
|
||||
Image: "images/contractconditions/condition_contrac_time_limit.jpg",
|
||||
ObjectivesCategory: "secondary",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
IsCompleted: true,
|
||||
ContractConditionType: "Single",
|
||||
ObjectiveInfo: [
|
||||
{
|
||||
Type: "custom",
|
||||
Properties: {
|
||||
Id: "05080d1d-e3c4-4960-a087-661d141363eb",
|
||||
BriefingName: "$loc UI_CONTRACT_UGC_REQUIRED_EXIT_NAME",
|
||||
BriefingText: "$loc UI_CONTRACT_UGC_REQUIRED_EXIT_DESC",
|
||||
LongBriefingText:
|
||||
"$loc UI_CONTRACT_UGC_REQUIRED_EXIT_DESC",
|
||||
Image: "images/contractconditions/condition_contrac_required_exit.jpg",
|
||||
ObjectivesCategory: "primary",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
noDisguiseChanges,
|
||||
noPacifications,
|
||||
noRecordings,
|
||||
noMissedShots,
|
||||
headshotsOnly,
|
||||
targetsOnly,
|
||||
]
|
||||
}
|
||||
|
||||
/*
|
||||
const PISTOL_ELIM = {
|
||||
$any: {
|
||||
"?": {
|
||||
$or: [
|
||||
{
|
||||
$eq: ["$.#", "pistol"],
|
||||
},
|
||||
{
|
||||
$eq: ["$.#", "close_combat_pistol_elimination"],
|
||||
},
|
||||
],
|
||||
},
|
||||
in: ["$Value.KillMethodBroad", "$Value.KillMethodStrict"],
|
||||
},
|
||||
}
|
||||
|
||||
export class StateMachineGenerationContext {
|
||||
public createKillMethodStateMachine(): void {
|
||||
this.weaponTargetStateMachine = {
|
||||
_comment: `Eliminate ${this.targetId} using weapon`,
|
||||
Type: "statemachine",
|
||||
Id: this.weaponTargetStateMachineId,
|
||||
Category: "secondary",
|
||||
Definition: {
|
||||
Scope: "Hit",
|
||||
Context: {
|
||||
Targets: [this.targetId],
|
||||
},
|
||||
States: {
|
||||
Start: {
|
||||
Kill: [
|
||||
{
|
||||
Condition: {
|
||||
$and: [
|
||||
{
|
||||
$eq: [
|
||||
"$Value.RepositoryId",
|
||||
this.targetId,
|
||||
],
|
||||
},
|
||||
{
|
||||
$eq: [
|
||||
"$Value.KillMethodStrict",
|
||||
this.genContextParams
|
||||
.KillMethodStrict,
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
Transition: "Success",
|
||||
},
|
||||
{
|
||||
Condition: {
|
||||
$eq: ["$Value.RepositoryId", this.targetId],
|
||||
},
|
||||
Transition: "Failure",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
private kmConditions(killMethodBroad) {
|
||||
switch (killMethodBroad) {
|
||||
case "pistol":
|
||||
return PISTOL_ELIM
|
||||
default:
|
||||
// weapon
|
||||
return {
|
||||
$eq: [
|
||||
"$Value.KillMethodStrict",
|
||||
this.genContextParams.KillMethodStrict,
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
62
components/contracts/elusiveTargets.ts
Normal file
62
components/contracts/elusiveTargets.ts
Normal file
@ -0,0 +1,62 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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/>.
|
||||
*/
|
||||
|
||||
export const orderedETs = [
|
||||
"8813e0a4-08ac-494f-a847-687a2da3582f",
|
||||
"ff188c8b-e1eb-4c59-af75-6b6fe3da5955",
|
||||
"0fd17346-bcb4-4bcc-acc3-5e1b6b184ef4",
|
||||
"13680605-83ed-4b8c-a44d-30cc5b4fb17a",
|
||||
"0d938ef9-05c7-4eb8-89cc-ae79b73c6992",
|
||||
"e87217e3-4809-4855-80d5-74bed66be58d",
|
||||
"8f13ea71-b207-4955-9eb8-ede757f3baa6",
|
||||
"158b600a-6448-45d3-907f-77351b9656ee",
|
||||
"2b928d67-c244-4601-bafb-7af664fb17bb",
|
||||
"a9d93d2a-c541-49ab-8ba1-9e345cf7e806",
|
||||
"ad549098-eb3d-4132-8ef8-fe77c6afbbaa",
|
||||
"16d78245-5392-413c-b3db-989d6685c32a",
|
||||
"b0bed170-8652-4188-8b9a-92caf9f97e5b",
|
||||
"92a87b10-a230-4986-bb35-06f16e84b11f",
|
||||
"c3c7126e-32cd-4502-b5ce-90b5ae436806",
|
||||
"0dc242ce-084e-4f6d-980f-e65885cd6955",
|
||||
"b0b8995c-7b3f-4fa6-91a2-be4bc8edc046",
|
||||
"550c4d75-ca87-4be7-a18e-caf30e6c8136",
|
||||
"5dc115d3-e5d4-4023-a11a-27c6f7194bea",
|
||||
"87f8293a-29cd-4cb1-ade7-dd6bb056d38e",
|
||||
"1c0377f3-6e32-4563-8baf-9677cdb3bb60",
|
||||
"655c5a57-69d1-48b6-a14b-2ae396c16174",
|
||||
"0fea5e55-9aec-41ef-9e5b-4e5e5f536f82",
|
||||
"b555d6a4-8b4d-4e1e-b6bd-ebd135ad1e01",
|
||||
"deace35f-ab6d-44c9-b1a6-98757e854f74",
|
||||
"2e2c3f33-92ad-412f-a351-b7267697ff70",
|
||||
"8462b2e5-4d34-4300-896f-fe1dc98fa877",
|
||||
"3716b654-a42c-45df-9db9-61795a6a3e46",
|
||||
"06a58b66-56f4-45c3-ba1b-d03998212289",
|
||||
"01e38e22-b8d8-4266-af3b-f3330c41e6f2",
|
||||
"263eca3d-d25d-40ce-ba0a-48a221cd0b9e",
|
||||
"44fd7474-d7be-4d3d-b944-6c1cf6ca09d1",
|
||||
"ecf353e8-3dd8-4958-b255-f963926aea51",
|
||||
"332e588b-80a3-4cb0-abc6-dc8de3d89e83",
|
||||
"cbc86bed-51ce-4699-89d4-0ded8f200cbc",
|
||||
"92951377-419d-4c31-aa21-2a3f03ef82d0",
|
||||
"fa002472-2120-44b6-bf48-41d14af97f51",
|
||||
"38dba4d9-a361-46c9-bdae-7350945d6526",
|
||||
"d8219c26-4122-4dde-bc42-382cdb374090",
|
||||
"1fcaff1b-7fa3-4b9f-a586-9c7a1689b48d",
|
||||
"b2c0251e-1803-4e12-b860-b9fa6ce5c004",
|
||||
"6fad7901-279f-45df-ab8d-087a3cb06dcc",
|
||||
]
|
386
components/contracts/escalationMappings.ts
Normal file
386
components/contracts/escalationMappings.ts
Normal file
@ -0,0 +1,386 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* An escalation group.
|
||||
* Format: level number -> contract ID.
|
||||
*/
|
||||
export type EscalationGroup = Record<number, string>
|
||||
|
||||
/*
|
||||
* /profiles/pages/Planning -> type = "escalation", New escalation only values
|
||||
* (InGroup, EscalationCompletedLevels, EscalationTotalLevels, "IsNew" on Objectives, etc..)
|
||||
* /authentication/api/userchannel/ContractsService/GetForPlay2 -> "NextContractId" pointing to the next escalation level,
|
||||
* "GroupData" with data of the entire escalation
|
||||
*/
|
||||
|
||||
/**
|
||||
* A mapping of escalation group ID to a mapping of escalation level numbers to contract IDs.
|
||||
*/
|
||||
export const escalationMappings: {
|
||||
[groupId: string]: EscalationGroup
|
||||
} = {
|
||||
/**
|
||||
* Berlin Egg Hunt
|
||||
*/
|
||||
"9d88605f-6871-46a8-bd46-9804ea04fca9": {
|
||||
1: "5452c904-e7e2-4cf4-939c-d3b41dd8dfb8",
|
||||
2: "a9e69460-73f2-4928-806d-f79d9e6368bc",
|
||||
3: "f5ebd915-3fc8-4cb7-95fd-f666f98e8b45",
|
||||
},
|
||||
/**
|
||||
* The Dexter Discordance
|
||||
*/
|
||||
"e96fb040-a13f-466c-9d96-c8f3b2b8a09a": {
|
||||
1: "d5e97d48-e58b-4d43-be35-ec29a51df452",
|
||||
2: "3e94d080-c6e4-4a2d-9a7d-74322440c877",
|
||||
3: "5db4c764-7ab7-40c1-8688-e2b98176fa35",
|
||||
},
|
||||
/**
|
||||
* The KOats Conspiracy
|
||||
*/
|
||||
"07ffa72a-bbac-45ca-8c9f-b9c1b526153a": {
|
||||
1: "a68b6d02-c769-4b22-a470-c7b88f3f3978",
|
||||
2: "864b5daa-1322-40f4-9708-04b5eee35317",
|
||||
3: "3b160b4f-1222-40a1-9a67-423c05b32340",
|
||||
},
|
||||
/**
|
||||
* The Proloff Parable
|
||||
*/
|
||||
"078a50d1-6427-4fc3-9099-e46390e637a0": {
|
||||
1: "645c9dd8-19e6-4cce-87ab-0e731fbaeab9",
|
||||
2: "20156bab-35f4-4a61-96f8-271041e38bf6",
|
||||
3: "40651beb-edaa-41d0-aa9d-6bd4a14a8f81",
|
||||
},
|
||||
/**
|
||||
* The Gauchito Antiquity
|
||||
*/
|
||||
"72aaaa7b-4386-4ee7-9e9e-73fb8ff8e416": {
|
||||
1: "e14bbb5d-bd8a-4b6b-9749-4f147db0ebe0",
|
||||
2: "95e7f0d5-f066-4f8c-bfc6-6505c13055ed",
|
||||
3: "3849e8d5-3876-48ef-b4e1-9b3a4489589a",
|
||||
},
|
||||
/**
|
||||
* The Corky Commotion
|
||||
*/
|
||||
"4f6ee6ec-b6d7-4958-9838-0352c10294a0": {
|
||||
1: "f89027eb-8ed9-49e3-8bb4-a6306f72e3d9",
|
||||
2: "2eb41963-a140-4ecb-9a05-327d4fd65408",
|
||||
3: "5cb1a153-3b56-417a-8e75-10066bf397b6",
|
||||
},
|
||||
/**
|
||||
* The Cheveyo Calibration
|
||||
*/
|
||||
"b49de2a1-fe8e-49c4-8331-17aaa9d65d32": {
|
||||
1: "d201ebf6-adc7-4d6f-87a4-f3d37a116a1b",
|
||||
2: "ba68c0d7-d77d-44b4-9401-72b2ff2d73cb",
|
||||
3: "e2dd58f3-f5ff-41b3-9ba9-4d0420fc773b",
|
||||
},
|
||||
/**
|
||||
* The Dalton Dissection
|
||||
*/
|
||||
"55063d85-e84a-4c76-8bf7-e70fe2cab651": {
|
||||
1: "44ac6a37-0ef8-42ea-bf39-1d5f9afd235d",
|
||||
2: "9a25fade-424f-481a-86f0-8c827d43b62e",
|
||||
3: "5adfcf3a-0696-4593-b755-c2c8d44f59a6",
|
||||
},
|
||||
/**
|
||||
* The CurryMaker Chaos
|
||||
*/
|
||||
"115425b1-e797-47bf-b517-410dc7507397": {
|
||||
1: "cac9f9c2-1a31-4ed8-b2f3-0da9bf5e515e",
|
||||
2: "3ef4e3b0-36f3-4c9e-a7bb-e98ae067b41a",
|
||||
3: "af09780c-eee9-4478-932c-e21c7bbe10b5",
|
||||
},
|
||||
/**
|
||||
* The Khakiasp Documentation
|
||||
*/
|
||||
"667f48a3-7f6b-486e-8f6b-2f782a5c4857": {
|
||||
1: "0677d534-b3eb-46f9-af67-23ff27b8475f",
|
||||
2: "b4934ad7-ef15-44cd-9ba1-7752755788b4",
|
||||
3: "7153609a-d24a-4f44-905d-d33d0b0b9a73",
|
||||
},
|
||||
/**
|
||||
* The Yannini Yearning
|
||||
*/
|
||||
"1e4423b7-d4ff-448f-a8a8-4bb600cab7e3": {
|
||||
1: "c6490c57-a033-4c6d-beed-cf4c8c7be552",
|
||||
2: "176e578f-3572-4fd0-9314-71b021ba1bad",
|
||||
3: "34674ed1-e76e-45cc-b575-e6f3f520bf7b",
|
||||
},
|
||||
/**
|
||||
* The Baskerville Barney
|
||||
*/
|
||||
"b12d08ea-c842-498a-82ea-889653588592": {
|
||||
1: "087d118b-57c7-4f52-929c-0e567ede6f5d",
|
||||
2: "19bf57ec-47c1-46b7-9e7f-ecc8309ae0c2",
|
||||
3: "77bcec76-323d-4e1e-bd0e-bf6d777c3745",
|
||||
},
|
||||
/**
|
||||
* The Jeffrey Consultation
|
||||
*/
|
||||
"0cceeecb-c8fe-42a4-aee4-d7b575f56a1b": {
|
||||
1: "408d03c6-46db-45f4-ab05-9f380eae4670",
|
||||
2: "4c41ac07-ad1d-47ff-a2db-3df85108b9b0",
|
||||
3: "d71b56ad-4134-4fb6-8e46-a7377a0e2a54",
|
||||
},
|
||||
/**
|
||||
* The mendietinha Madness
|
||||
*/
|
||||
"ccdc7043-62af-44e8-a5fc-38b008c2044e": {
|
||||
1: "b7401d91-7705-40c9-84a3-bf8f236444de",
|
||||
2: "6ee9d8b0-d0db-426d-bbf6-64a2983b274c",
|
||||
3: "ffb1da03-fcbf-4d7f-8371-de685498516e",
|
||||
},
|
||||
/**
|
||||
* The Turms Infatuation
|
||||
*/
|
||||
"0042ab2c-8aa3-48e5-a75f-4558c691adff": {
|
||||
1: "cbdd649b-bada-441c-9b0d-1e2d7849b055",
|
||||
2: "0d84d2e9-9b2b-4801-bee6-80adf3afe5e6",
|
||||
3: "b0719294-b3ca-11eb-8529-0242ac130003",
|
||||
},
|
||||
/**
|
||||
* The Delgado Larceny
|
||||
*/
|
||||
"11e632a1-e246-4641-927b-6fd7daf83016": {
|
||||
1: "4e846e60-c98b-4581-9487-083c0353b5a7",
|
||||
2: "af558186-5ca1-41b9-ab87-b854345b77b5",
|
||||
3: "4f726edc-a0dd-11eb-bcbc-0242ac130002",
|
||||
},
|
||||
/**
|
||||
* The Calvino Cacophony
|
||||
*/
|
||||
"d7cac2f8-e870-4e68-92ba-19b6a88d1053": {
|
||||
1: "d265e641-dfaa-4b91-8f5f-227a8bed947a",
|
||||
2: "ede95e7f-36b1-4c1b-a4c5-fba9edee296d",
|
||||
3: "031097b4-b17f-11eb-8529-0242ac130003",
|
||||
},
|
||||
/**
|
||||
* The Merle Revelation
|
||||
*/
|
||||
"3bdf8b88-c795-4f30-aa69-c04c3d05d8ce": {
|
||||
1: "68ac028f-e83f-4496-95a2-eb3c5b8825c9",
|
||||
2: "50a56b1f-668f-402d-a6b7-0f759b33ca56",
|
||||
3: "b0718c68-b3ca-11eb-8529-0242ac130003",
|
||||
},
|
||||
/**
|
||||
* The MacMillan Surreptition
|
||||
*/
|
||||
"e88c9be7-a802-40b4-b2ae-487b3d047e2c": {
|
||||
1: "9f18dff5-6412-4240-91e4-4170d816c0fe",
|
||||
2: "c976b9ea-1921-4ce9-8651-dce488ffeb36",
|
||||
3: "0310987c-b17f-11eb-8529-0242ac130003",
|
||||
},
|
||||
/**
|
||||
* The Montague Audacity
|
||||
*/
|
||||
"256845d8-d8dd-4073-a69a-e5c0ddb3ff61": {
|
||||
1: "309eba43-f514-4ed1-ab9e-9f76547f4b6f",
|
||||
2: "69b8a95b-03ff-4d4c-89b1-eb0ca4dbe6c0",
|
||||
3: "b0718f1a-b3ca-11eb-8529-0242ac130003",
|
||||
},
|
||||
/**
|
||||
* The Truman Contravention
|
||||
*/
|
||||
"35b6a403-54f4-4faa-9b19-448d6840d837": {
|
||||
1: "42c11cac-309c-47ae-a293-ee8bde6918ab",
|
||||
2: "31516bfe-694d-418f-89eb-c9b4740af5dd",
|
||||
3: "b071900a-b3ca-11eb-8529-0242ac130003",
|
||||
},
|
||||
/**
|
||||
* The Ataro Caliginosity
|
||||
*/
|
||||
"c2e16fb7-d49f-49ef-9d76-46b8b31b3389": {
|
||||
1: "044cb8a3-bb83-4484-811a-7644ae1f7b8b",
|
||||
2: "c3322acb-bb6c-4f3f-a48d-a654aea83ec7",
|
||||
3: "a2e4d7e7-f9e3-4e37-ae56-6739a6f17a4f",
|
||||
4: "f4bec62f-0fd6-4071-9bc7-003a5260118b",
|
||||
5: "8eed3a7f-b903-412d-85b6-e4262e7246d7",
|
||||
},
|
||||
/**
|
||||
* The McVeigh Ascension
|
||||
*/
|
||||
"9e0188e8-bdad-476c-b4ce-2faa5d2be56c": {
|
||||
1: "b5f3a898-fb25-4988-b530-a32ec5b6bad5",
|
||||
2: "3c882fd9-63ee-4981-abf3-006a1335c04d",
|
||||
3: "4ccf2b51-4a99-4a6d-a37c-31ef5d27e703",
|
||||
},
|
||||
/**
|
||||
* The Mills Reverie
|
||||
*/
|
||||
"3efc73f9-33f0-4af6-9508-7208e6851394": {
|
||||
1: "8b3241a8-3a71-43c2-a9b2-2282271ad01e",
|
||||
2: "3dd4effa-c919-471d-a3ee-becf7504ce82",
|
||||
3: "97c4148b-ecea-4735-87cd-563e9a4ad343",
|
||||
},
|
||||
/**
|
||||
* The Dubious Cohabitation
|
||||
*/
|
||||
"e302a045-0250-4824-9416-675cf936e035": {
|
||||
1: "987c40f7-bf23-4f8d-84d6-169101edf953",
|
||||
2: "aa3afd89-e080-4bee-83fe-87e26fbd7e3a",
|
||||
3: "b071941a-b3ca-11eb-8529-0242ac130003",
|
||||
},
|
||||
/**
|
||||
* The Dartmoor Garden Show
|
||||
*/
|
||||
"5680108a-19dc-4448-9344-3d0290217162": {
|
||||
1: "bdd43a59-b74f-4159-8e7d-7209e5a13f84",
|
||||
2: "cef8d7c3-35e5-44b4-8c41-3b0f074bf8cd",
|
||||
3: "f7ad71b6-9553-4d58-86dc-e3e288849849",
|
||||
},
|
||||
/**
|
||||
* The Barbegue Befuddlement
|
||||
*/
|
||||
"448d89e8-2026-43e3-86f0-205018cbd87e": {
|
||||
1: "b82fd894-c12c-44e9-99fd-07b860b76c72",
|
||||
2: "2d1bada4-aa46-4954-8cf5-684989f1668a",
|
||||
3: "519c097f-2e1f-48f2-8f9d-3c76223cc950",
|
||||
},
|
||||
/**
|
||||
* The Pirates Problem
|
||||
*/
|
||||
"f19f7ac8-39ec-498b-aa23-44c8e75d8693": {
|
||||
1: "88725ca6-cf32-41e5-bd18-1c2c9aafd8aa",
|
||||
2: "3f5c032b-1429-455e-acfd-5ceab5a4e26d",
|
||||
3: "bdd4bdee-6720-44c2-908d-769f58c0cf12",
|
||||
},
|
||||
/**
|
||||
* The Susumu Obsession
|
||||
*/
|
||||
"85a2b618-2e3c-444f-931c-b89d566e45f7": {
|
||||
1: "ae4db4c3-32bb-4717-8df3-83d8f77a6d0f",
|
||||
2: "7f5d1e2a-9c89-48c2-a370-85d851c3cc21",
|
||||
3: "6b1fcdc7-e2c9-48c4-b1fb-0a8dd817f3b2",
|
||||
4: "d80abc24-f7d5-4e6b-a6c2-fd318135d160",
|
||||
5: "b007a400-66b8-43c3-a919-3195e343f7b1",
|
||||
},
|
||||
/**
|
||||
* The Marinello Motivation
|
||||
*/
|
||||
"d0a0fa03-08a7-43ef-b5e8-d8662d015372": {
|
||||
1: "f33d4dee-8d07-45e0-9816-55646dcb341f",
|
||||
2: "aa4fb2e6-3494-4b88-a882-43ce135f8b1b",
|
||||
3: "9f17c5ee-b402-11eb-8529-0242ac130003",
|
||||
},
|
||||
/**
|
||||
* The Sinbad Stringent
|
||||
*/
|
||||
"be14d4f1-f1aa-4dea-8c9b-a5b1a1dea931": {
|
||||
1: "b1f59afe-1b57-470d-80a1-982cb37e0c05",
|
||||
2: "4396c59b-9fa2-46ab-8cc8-0bd782225054",
|
||||
3: "e928e04a-922f-462a-9b44-0f8e42a05102",
|
||||
},
|
||||
/**
|
||||
* The Agana Abyss
|
||||
*/
|
||||
"74739eda-6ed5-4318-a501-2fa0bd53ef5a": {
|
||||
1: "9943bcc6-8897-42b9-93eb-12ff5be8b7ac",
|
||||
2: "5548a549-3c55-4014-8cc7-47145f7f75d6",
|
||||
3: "bab3704b-0bbb-4d0d-b5bf-ebf715f419cd",
|
||||
4: "0434d0ac-5e74-4d1f-8aef-8abbadabd1aa",
|
||||
5: "4ed3bdfe-ecfc-41ab-ba6d-25d053838e15",
|
||||
},
|
||||
/**
|
||||
* The dez Dichotomy
|
||||
*/
|
||||
"78628e05-93ce-4f87-8a17-b910d32df51f": {
|
||||
1: "4804d74a-96d9-40af-8a37-a1f377781fc1",
|
||||
2: "1a9978ae-0cfa-44ff-bd16-ca3ffab226fe",
|
||||
3: "0fc24d6e-5870-44d3-897a-15f19c4ccef2",
|
||||
},
|
||||
/**
|
||||
* The Caden Composition
|
||||
*/
|
||||
"ccbde3e2-67e7-4534-95ec-e9bd7ef65273": {
|
||||
1: "3c93370d-e6d2-48b3-b37a-8fa27f63027c",
|
||||
2: "4e1cac0d-0f16-4c58-ad8c-f5dc003fe368",
|
||||
3: "ed54d12a-51e3-470d-b712-cb2a364c95d0",
|
||||
4: "79511294-9054-409d-8062-c24d66fb1ff0",
|
||||
5: "2e83eda3-230f-429c-965a-c89e7ada97e3",
|
||||
},
|
||||
/**
|
||||
* The PapaLevy Plunderage
|
||||
*/
|
||||
"9a461f89-86c5-44e4-998e-f2f66b496aa7": {
|
||||
1: "5e380d27-930d-4bc7-9ad9-411486a7147c",
|
||||
2: "d93d8114-3284-4306-80c5-117fa03de533",
|
||||
3: "6968e2a0-b7cf-4cb8-8da5-4871d8c564a5",
|
||||
},
|
||||
/**
|
||||
* The Aquatic Retribution
|
||||
*/
|
||||
"69b8eb0c-77d5-42e8-b604-26aba8bd835f": {
|
||||
1: "e155844a-032c-4b71-91a4-b1206e0f6a8c",
|
||||
2: "3063ccc6-9fa5-439f-9a9f-72a1b81c369e",
|
||||
3: "4a0a66d4-0a53-4cfd-8122-978226b4e072",
|
||||
},
|
||||
/**
|
||||
* The Dammchicu Disaster
|
||||
*/
|
||||
"218302a3-f682-46f9-9ffd-bb3e82487b7c": {
|
||||
1: "9d0b3322-4dd4-4388-b79f-4aae7dba297c",
|
||||
2: "b07af7b6-01cb-4cee-83bf-2c73f71bf2a3",
|
||||
3: "45b1b927-5bf0-4dae-bc73-8ee1730652cc",
|
||||
},
|
||||
/**
|
||||
* The Holmwood Disturbance
|
||||
*/
|
||||
"d6961637-effe-4c39-b99a-f2df4402657d": {
|
||||
1: "30f4a862-35f8-4f34-ba0d-552ac87ccbbe",
|
||||
2: "1e307dd6-74cb-4e4a-9829-50106a95c3ef",
|
||||
3: "6cc11563-1101-4c63-9d24-483fca17915b",
|
||||
4: "dbd36aab-ed98-47e2-823a-867ba6e070d1",
|
||||
5: "4aba4161-608c-4f2c-b1b7-e6669a5eac44",
|
||||
},
|
||||
/**
|
||||
* The PurpleKey Peril
|
||||
*/
|
||||
"74415eca-d01e-4070-9bc9-5ef9b4e8f7d2": {
|
||||
1: "f556bfdd-3be1-4f1b-9a41-5c6747766262",
|
||||
2: "97ab1c9a-3236-41f0-b22e-b46728ffc9fd",
|
||||
3: "490cbf23-92ca-4cdb-b301-9a576442ad2b",
|
||||
},
|
||||
/**
|
||||
* The Hamartia Compulsion
|
||||
*/
|
||||
"4b6739eb-bcdb-48ad-8c45-a829794175e1": {
|
||||
1: "56684a64-70c1-4845-a2c4-b49ddd78a45e",
|
||||
2: "7d911fca-b4bb-4b31-8564-5f5fd7b82a9b",
|
||||
3: "daf0ab18-dc1b-481b-9731-3aec536f231f",
|
||||
4: "e11b70db-f436-4eee-ac69-8d76eb1d3e9d",
|
||||
5: "d48ed63e-2542-4215-8e6c-6ad3c29feb42",
|
||||
},
|
||||
/**
|
||||
* The Argentine Acrimony
|
||||
*/
|
||||
"edbacf4b-e402-4548-b723-cd4351571537": {
|
||||
1: "4bab4282-ad93-45e3-ace7-c9daf78dec94",
|
||||
2: "1e96f1f2-eaf7-4947-a60d-d2190f503b0b",
|
||||
3: "a7ddf3f3-7fd9-4749-b63b-f2579bbd0f6c",
|
||||
},
|
||||
/**
|
||||
* The sleazeball Situation
|
||||
*/
|
||||
"35f1f534-ae2d-42be-8472-dd55e96625ea": {
|
||||
1: "3edb330f-5129-49c6-9afd-70111ce72ae5",
|
||||
2: "c59baa15-5946-4354-875e-1c98ef7f1bfe",
|
||||
3: "83655c86-012f-4d2b-a57d-5b021af99af1",
|
||||
},
|
||||
}
|
193
components/contracts/escalations/escalationService.ts
Normal file
193
components/contracts/escalations/escalationService.ts
Normal file
@ -0,0 +1,193 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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 { contractIdToHitObject, controller } from "../../controller"
|
||||
import type { GameVersion, IHit, UserProfile } from "../../types/types"
|
||||
import { getUserData } from "../../databaseHandler"
|
||||
import { log, LogLevel } from "../../loggingInterop"
|
||||
import type { EscalationGroup } from "../escalationMappings"
|
||||
|
||||
/**
|
||||
* Put a level in here to hide it from the menus on 2016.
|
||||
* This should only be used if:
|
||||
* - The content is custom.
|
||||
* - The content is on a 2016 map.
|
||||
*/
|
||||
const no2016 = [
|
||||
"0cceeecb-c8fe-42a4-aee4-d7b575f56a1b",
|
||||
"9e0188e8-bdad-476c-b4ce-2faa5d2be56c",
|
||||
"115425b1-e797-47bf-b517-410dc7507397",
|
||||
"74415eca-d01e-4070-9bc9-5ef9b4e8f7d2",
|
||||
]
|
||||
|
||||
/**
|
||||
* Gets a user's progress on the specified escalation's group ID.
|
||||
*
|
||||
* @param userData The user's profile object.
|
||||
* @param eGroupId The escalation's group ID.
|
||||
* @returns The level of the escalation the user is on.
|
||||
*/
|
||||
export function getUserEscalationProgress(
|
||||
userData: UserProfile,
|
||||
eGroupId: string,
|
||||
): number {
|
||||
userData.Extensions.PeacockEscalations ??= { [eGroupId]: 1 }
|
||||
|
||||
if (!userData.Extensions.PeacockEscalations[eGroupId]) {
|
||||
userData.Extensions.PeacockEscalations[eGroupId] = 1
|
||||
return 1
|
||||
}
|
||||
|
||||
return userData.Extensions.PeacockEscalations[eGroupId]
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets a user's progress on the specified escalation.
|
||||
*
|
||||
* @param userData The user's profile object.
|
||||
* @param eGroupId The escalation's group ID.
|
||||
*/
|
||||
export function resetUserEscalationProgress(
|
||||
userData: UserProfile,
|
||||
eGroupId: string,
|
||||
): void {
|
||||
userData.Extensions.PeacockEscalations ??= {}
|
||||
|
||||
userData.Extensions.PeacockEscalations[eGroupId] = 1
|
||||
|
||||
if (userData.Extensions.PeacockCompletedEscalations?.includes(eGroupId)) {
|
||||
userData.Extensions.PeacockCompletedEscalations =
|
||||
userData.Extensions.PeacockCompletedEscalations!.filter(
|
||||
(e) => e !== eGroupId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates a contract ID to the escalation group that it is in's ID.
|
||||
*
|
||||
* @param id The contract ID.
|
||||
* @returns The escalation's group ID or null if it isn't in a group.
|
||||
*/
|
||||
export function contractIdToEscalationGroupId(id: string): string | undefined {
|
||||
let name: string | undefined = undefined
|
||||
|
||||
for (const groupId of Object.keys(controller.escalationMappings)) {
|
||||
for (const level of Object.keys(
|
||||
controller.escalationMappings[groupId],
|
||||
)) {
|
||||
if (controller.escalationMappings[groupId][level].includes(id)) {
|
||||
name = groupId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
export function getMenuDetailsForEscalation(
|
||||
eGroupId: string,
|
||||
userId: string,
|
||||
gameVersion: GameVersion,
|
||||
): IHit | undefined {
|
||||
const userData = getUserData(userId, gameVersion)
|
||||
|
||||
const level = getUserEscalationProgress(userData, eGroupId)
|
||||
const escalationGroup = controller.escalationMappings[eGroupId]
|
||||
|
||||
if (gameVersion === "h1" && no2016.includes(eGroupId)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// not great for performance, but it's fine for now
|
||||
if (!controller.resolveContract(escalationGroup[level])) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return contractIdToHitObject(escalationGroup[level], gameVersion, userId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of levels in the specified group.
|
||||
*
|
||||
* @param group The escalation group.
|
||||
* @returns The number of levels.
|
||||
*/
|
||||
export function getLevelCount(group: EscalationGroup): number {
|
||||
let levels = 1
|
||||
|
||||
while (Object.prototype.hasOwnProperty.call(group, levels + 1)) {
|
||||
levels++
|
||||
}
|
||||
|
||||
return levels
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the "play" escalation info.
|
||||
*
|
||||
* @param b Internal control for code cleanliness. Setting to false returns an empty object.
|
||||
* @param userId The current user's ID.
|
||||
* @param eGroupId The escalation's group ID.
|
||||
* @param gameVersion The game's version.
|
||||
* @returns The escalation play details.
|
||||
*/
|
||||
export function getPlayEscalationInfo(
|
||||
b: boolean,
|
||||
userId: string,
|
||||
eGroupId: string,
|
||||
gameVersion: GameVersion,
|
||||
) {
|
||||
if (!b) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const userData = getUserData(userId, gameVersion)
|
||||
|
||||
const p = getUserEscalationProgress(userData, eGroupId)
|
||||
const group = controller.escalationMappings[eGroupId]
|
||||
|
||||
const totalLevelCount = getLevelCount(
|
||||
controller.escalationMappings[eGroupId],
|
||||
)
|
||||
|
||||
let nextContractId = "00000000-0000-0000-0000-000000000000"
|
||||
if (p < totalLevelCount) {
|
||||
nextContractId = group[p + 1]
|
||||
}
|
||||
|
||||
log(
|
||||
LogLevel.DEBUG,
|
||||
`Get Play-EscalationInfo - group: ${eGroupId} prog: ${p} next: ${nextContractId}`,
|
||||
)
|
||||
|
||||
return {
|
||||
Type: "escalation",
|
||||
InGroup: eGroupId,
|
||||
NextContractId: nextContractId,
|
||||
GroupData: {
|
||||
Level: p,
|
||||
TotalLevels: totalLevelCount,
|
||||
Completed:
|
||||
userData.Extensions.PeacockCompletedEscalations?.includes(
|
||||
eGroupId,
|
||||
) || false,
|
||||
FirstContractId: group[1],
|
||||
},
|
||||
}
|
||||
}
|
212
components/contracts/hitsCategoryService.ts
Normal file
212
components/contracts/hitsCategoryService.ts
Normal file
@ -0,0 +1,212 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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 { HookMap, SyncHook } from "../hooksImpl"
|
||||
import { GameVersion, HitsCategoryCategory } from "../types/types"
|
||||
import {
|
||||
contractIdToHitObject,
|
||||
controller,
|
||||
featuredContractGroups,
|
||||
} from "../controller"
|
||||
import { getUserData } from "../databaseHandler"
|
||||
import { orderedETs } from "./elusiveTargets"
|
||||
|
||||
function paginate<Element>(
|
||||
elements: Element[],
|
||||
displayPerPage: number,
|
||||
): Element[][] {
|
||||
const totalElementCount: number = elements.length
|
||||
const pageCount = Math.ceil(totalElementCount / displayPerPage)
|
||||
const pages: Element[][] = []
|
||||
let perPageArray: Element[] = []
|
||||
let index = 0
|
||||
let condition = 0
|
||||
let pendingDispatchCount = 0
|
||||
|
||||
for (let i = 0; i < pageCount; i++) {
|
||||
if (i === 0) {
|
||||
index = 0
|
||||
condition = displayPerPage
|
||||
}
|
||||
|
||||
for (let j = index; j < condition; j++) {
|
||||
perPageArray.push(elements[j])
|
||||
}
|
||||
|
||||
pages.push(perPageArray)
|
||||
|
||||
if (i === 0) {
|
||||
pendingDispatchCount = totalElementCount - perPageArray.length
|
||||
} else {
|
||||
pendingDispatchCount = pendingDispatchCount - perPageArray.length
|
||||
}
|
||||
|
||||
if (pendingDispatchCount > 0) {
|
||||
if (pendingDispatchCount > displayPerPage) {
|
||||
index = index + displayPerPage
|
||||
condition = condition + displayPerPage
|
||||
} else {
|
||||
index = index + perPageArray.length
|
||||
condition = condition + pendingDispatchCount
|
||||
}
|
||||
}
|
||||
|
||||
perPageArray = []
|
||||
}
|
||||
|
||||
return pages
|
||||
}
|
||||
|
||||
export class HitsCategoryService {
|
||||
/**
|
||||
* A hook map for all the hits categories.
|
||||
*/
|
||||
public hitsCategories: HookMap<
|
||||
SyncHook<
|
||||
[
|
||||
/** gameVersion */ GameVersion,
|
||||
/** contractIds */ string[],
|
||||
/** hitsCategory */ HitsCategoryCategory,
|
||||
/** userId */ string,
|
||||
]
|
||||
>
|
||||
>
|
||||
|
||||
/**
|
||||
* Hits categories that should not be automatically paginated.
|
||||
*/
|
||||
public paginationExempt = ["Elusive_Target_Hits", "Arcade", "Sniper"]
|
||||
|
||||
/**
|
||||
* The number of hits per page.
|
||||
*/
|
||||
public hitsPerPage = 22
|
||||
|
||||
constructor() {
|
||||
this.hitsCategories = new HookMap(() => new SyncHook())
|
||||
|
||||
this._useDefaultHitsCategories()
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable the default hits categories.
|
||||
*/
|
||||
_useDefaultHitsCategories(): void {
|
||||
const tapName = "HitsCategoryServiceImpl"
|
||||
|
||||
this.hitsCategories
|
||||
.for("Sniper")
|
||||
.tap(tapName, (gameVersion, contracts) => {
|
||||
contracts.push("ff9f46cf-00bd-4c12-b887-eac491c3a96d")
|
||||
contracts.push("00e57709-e049-44c9-a2c3-7655e19884fb")
|
||||
contracts.push("25b20d86-bb5a-4ebd-b6bb-81ed2779c180")
|
||||
})
|
||||
|
||||
this.hitsCategories
|
||||
.for("Elusive_Target_Hits")
|
||||
.tap(tapName, (gameVersion, contracts) => {
|
||||
contracts.push(...orderedETs)
|
||||
})
|
||||
|
||||
this.hitsCategories
|
||||
.for("MyContracts")
|
||||
.tap(tapName, (gameVersion, contracts, hitsCategory) => {
|
||||
hitsCategory.CurrentSubType = "MyContracts"
|
||||
|
||||
for (const contract of controller.contracts.values()) {
|
||||
contracts.push(contract.Metadata.Id)
|
||||
}
|
||||
})
|
||||
|
||||
this.hitsCategories
|
||||
.for("Featured")
|
||||
.tap(tapName, (gameVersion, contracts) => {
|
||||
for (const fcGroup of featuredContractGroups) {
|
||||
contracts.push(...fcGroup)
|
||||
}
|
||||
})
|
||||
|
||||
this.hitsCategories
|
||||
.for("MyPlaylist")
|
||||
.tap(tapName, (gameVersion, contracts, hitsCategory, userId) => {
|
||||
const userProfile = getUserData(userId, gameVersion)
|
||||
const favs =
|
||||
userProfile?.Extensions.PeacockFavoriteContracts ?? []
|
||||
|
||||
contracts.push(...favs)
|
||||
|
||||
hitsCategory.CurrentSubType = "MyPlaylist_all"
|
||||
})
|
||||
|
||||
// intentionally don't handle Trending
|
||||
// intentionally don't handle MostPlayedLastWeek
|
||||
// intentionally don't handle Arcade
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a {@link HitsCategoryCategory} object for the current page.
|
||||
*
|
||||
* @param categoryName The hits category's ID (the key for the hooks map).
|
||||
* @param pageNumber The current page's number.
|
||||
* @param gameVersion The game version being used for the request.
|
||||
* @param userId The current user's ID.
|
||||
* @returns The {@link HitsCategoryCategory} object.
|
||||
*/
|
||||
public paginateHitsCategory(
|
||||
categoryName: string,
|
||||
pageNumber: number,
|
||||
gameVersion: GameVersion,
|
||||
userId: string,
|
||||
): HitsCategoryCategory {
|
||||
const hitsCategory: HitsCategoryCategory = {
|
||||
Category: categoryName,
|
||||
Data: {
|
||||
Type: categoryName,
|
||||
Hits: [],
|
||||
Page: pageNumber,
|
||||
HasMore: false,
|
||||
},
|
||||
CurrentSubType: categoryName,
|
||||
}
|
||||
|
||||
const hook = this.hitsCategories.for(categoryName)
|
||||
|
||||
const hits: string[] = []
|
||||
|
||||
hook.call(gameVersion, hits, hitsCategory, userId)
|
||||
|
||||
const hitObjectList = hits
|
||||
.map((id) => contractIdToHitObject(id, gameVersion, userId))
|
||||
.filter(Boolean)
|
||||
|
||||
if (!this.paginationExempt.includes(categoryName)) {
|
||||
const paginated = paginate(hitObjectList, this.hitsPerPage)
|
||||
|
||||
// ts-expect-error Type things.
|
||||
hitsCategory.Data.Hits = paginated[pageNumber]
|
||||
hitsCategory.Data.HasMore = paginated.length > pageNumber + 1
|
||||
} else {
|
||||
// ts-expect-error Type things.
|
||||
hitsCategory.Data.Hits = hitObjectList
|
||||
}
|
||||
|
||||
return hitsCategory
|
||||
}
|
||||
}
|
||||
|
||||
export const hitsCategoryService = new HitsCategoryService()
|
162
components/contracts/missionsInLocation.ts
Normal file
162
components/contracts/missionsInLocation.ts
Normal file
@ -0,0 +1,162 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A mapping of location ID to an array of missions IDs.
|
||||
*/
|
||||
export const missionsInLocations = {
|
||||
LOCATION_ICA_FACILITY_ARRIVAL: ["1436cbe4-164b-450f-ad2c-77dec88f53dd"],
|
||||
LOCATION_ICA_FACILITY_SHIP: [
|
||||
"1d241b00-f585-4e3d-bc61-3095af1b96e2",
|
||||
"b573932d-7a34-44f1-bcf4-ea8f79f75710",
|
||||
],
|
||||
LOCATION_ICA_FACILITY: ["ada5f2b1-8529-48bb-a596-717f75f5eacb"],
|
||||
LOCATION_PARIS: [
|
||||
"00000000-0000-0000-0000-000000000200",
|
||||
"4e45e91a-94ca-4d89-89fc-1b250e608e73",
|
||||
],
|
||||
LOCATION_COASTALTOWN: ["00000000-0000-0000-0000-000000000600"],
|
||||
LOCATION_COASTALTOWN_NIGHT: ["00000000-0000-0000-0001-000000000005"],
|
||||
LOCATION_COASTALTOWN_EBOLA: ["7e3f758a-2435-42de-93bd-d8f0b72c63a4"],
|
||||
LOCATION_COASTALTOWN_MOVIESET: ["00000000-0000-0000-0001-000000000006"],
|
||||
LOCATION_MARRAKECH: ["00000000-0000-0000-0000-000000000400"],
|
||||
LOCATION_MARRAKECH_NIGHT: ["ced93d8f-9535-425a-beb9-ef219e781e81"],
|
||||
LOCATION_BANGKOK: ["db341d9f-58a4-411d-be57-0bc4ed85646b"],
|
||||
LOCATION_BANGKOK_ZIKA: [
|
||||
"024b6964-a3bb-4457-b085-08f9a7dc7fb7",
|
||||
"90c291f6-7ac3-46de-99b2-082e38fccb24",
|
||||
],
|
||||
LOCATION_COLORADO: ["42bac555-bbb9-429d-a8ce-f1ffdf94211c"],
|
||||
LOCATION_COLORADO_RABIES: ["ada6205e-6ee8-4189-9cdb-4947cccd84f4"],
|
||||
LOCATION_HOKKAIDO: [
|
||||
"0e81a82e-b409-41e9-9e3b-5f82e57f7a12",
|
||||
"c414a084-a7b9-43ce-b6ca-590620acd87e",
|
||||
],
|
||||
LOCATION_HOKKAIDO_FLU: ["a2befcec-7799-4987-9215-6a152cb6a320"],
|
||||
LOCATION_NEWZEALAND: ["c65019e5-43a8-4a33-8a2a-84c750a5eeb3"],
|
||||
LOCATION_MIAMI: ["c1d015b4-be08-4e44-808e-ada0f387656f"],
|
||||
LOCATION_MIAMI_COTTONMOUTH: ["f1ba328f-e3dd-4ef8-bb26-0363499fdd95"],
|
||||
LOCATION_COLOMBIA: ["422519be-ed2e-44df-9dac-18f739d44fd9"],
|
||||
LOCATION_COLOMBIA_ANACONDA: ["179563a4-727a-4072-b354-c9fff4e8bff0"],
|
||||
LOCATION_MUMBAI: ["0fad48d7-3d0f-4c66-8605-6cbe9c3a46d7"],
|
||||
LOCATION_MUMBAI_KINGCOBRA: ["a8036782-de0a-4353-b522-0ab7a384bade"],
|
||||
LOCATION_NORTHAMERICA: ["82f55837-e26c-41bf-bc6e-fa97b7981fbc"],
|
||||
LOCATION_NORTHAMERICA_GARTERSNAKE: ["0b616e62-af0c-495b-82e3-b778e82b5912"],
|
||||
LOCATION_NORTHSEA: ["0d225edf-40cd-4f20-a30f-b62a373801d3"],
|
||||
LOCATION_GREEDY_RACCOON: ["7a03a97d-238c-48bd-bda0-e5f279569cce"],
|
||||
LOCATION_OPULENT_STINGRAY: ["095261b5-e15b-4ca1-9bb7-001fb85c5aaa"],
|
||||
LOCATION_GOLDEN_GECKO: ["7d85f2b0-80ca-49be-a2b7-d56f67faf252"],
|
||||
LOCATION_ANCESTRAL_BULLDOG: ["755984a8-fb0b-4673-8637-95cfe7d34e0f"],
|
||||
LOCATION_EDGY_FOX: ["ebcd14b2-0786-4ceb-a2a4-e771f60d0125"],
|
||||
LOCATION_WET_RAT: [
|
||||
"3d0cbb8c-2a80-442a-896b-fea00e98768c",
|
||||
"99bd3287-1d83-4429-a769-45045dfcbf31",
|
||||
],
|
||||
LOCATION_ELEGANT_LLAMA: ["d42f850f-ca55-4fc9-9766-8c6a2b5c3129"],
|
||||
LOCATION_TRAPPED_WOLVERINE: ["a3e19d55-64a6-4282-bb3c-d18c3f3e6e29"],
|
||||
LOCATION_ROCKY_DUGONG: ["b2aac100-dfc7-4f85-b9cd-528114436f6c"],
|
||||
escalations: {
|
||||
LOCATION_PARIS: [
|
||||
"4f6ee6ec-b6d7-4958-9838-0352c10294a0",
|
||||
"d6961637-effe-4c39-b99a-f2df4402657d",
|
||||
],
|
||||
LOCATION_COASTALTOWN: [
|
||||
"9e0188e8-bdad-476c-b4ce-2faa5d2be56c",
|
||||
"74415eca-d01e-4070-9bc9-5ef9b4e8f7d2",
|
||||
"4b6739eb-bcdb-48ad-8c45-a829794175e1",
|
||||
],
|
||||
LOCATION_COASTALTOWN_MOVIESET: ["74739eda-6ed5-4318-a501-2fa0bd53ef5a"],
|
||||
LOCATION_COASTALTOWN_EBOLA: ["0cceeecb-c8fe-42a4-aee4-d7b575f56a1b"],
|
||||
LOCATION_MARRAKECH_NIGHT: [
|
||||
"b49de2a1-fe8e-49c4-8331-17aaa9d65d32",
|
||||
"c2e16fb7-d49f-49ef-9d76-46b8b31b3389",
|
||||
],
|
||||
LOCATION_BANGKOK: ["ccbde3e2-67e7-4534-95ec-e9bd7ef65273"],
|
||||
LOCATION_HOKKAIDO: [
|
||||
"e96fb040-a13f-466c-9d96-c8f3b2b8a09a",
|
||||
"115425b1-e797-47bf-b517-410dc7507397",
|
||||
"85a2b618-2e3c-444f-931c-b89d566e45f7",
|
||||
],
|
||||
LOCATION_NEWZEALAND: ["3efc73f9-33f0-4af6-9508-7208e6851394"],
|
||||
LOCATION_MIAMI: [
|
||||
"448d89e8-2026-43e3-86f0-205018cbd87e",
|
||||
"69b8eb0c-77d5-42e8-b604-26aba8bd835f",
|
||||
],
|
||||
LOCATION_COLOMBIA: [
|
||||
"0042ab2c-8aa3-48e5-a75f-4558c691adff",
|
||||
"11e632a1-e246-4641-927b-6fd7daf83016",
|
||||
"d7cac2f8-e870-4e68-92ba-19b6a88d1053",
|
||||
"3bdf8b88-c795-4f30-aa69-c04c3d05d8ce",
|
||||
"e88c9be7-a802-40b4-b2ae-487b3d047e2c",
|
||||
"256845d8-d8dd-4073-a69a-e5c0ddb3ff61",
|
||||
"35b6a403-54f4-4faa-9b19-448d6840d837",
|
||||
],
|
||||
LOCATION_MUMBAI: [
|
||||
"667f48a3-7f6b-486e-8f6b-2f782a5c4857",
|
||||
"e302a045-0250-4824-9416-675cf936e035",
|
||||
],
|
||||
LOCATION_NORTHAMERICA: ["218302a3-f682-46f9-9ffd-bb3e82487b7c"],
|
||||
LOCATION_NORTHSEA: ["d0a0fa03-08a7-43ef-b5e8-d8662d015372"],
|
||||
LOCATION_GREEDY_RACCOON: [
|
||||
"55063d85-e84a-4c76-8bf7-e70fe2cab651",
|
||||
"9a461f89-86c5-44e4-998e-f2f66b496aa7",
|
||||
],
|
||||
LOCATION_OPULENT_STINGRAY: [
|
||||
"f19f7ac8-39ec-498b-aa23-44c8e75d8693",
|
||||
"35f1f534-ae2d-42be-8472-dd55e96625ea",
|
||||
],
|
||||
LOCATION_GOLDEN_GECKO: ["be14d4f1-f1aa-4dea-8c9b-a5b1a1dea931"],
|
||||
LOCATION_ANCESTRAL_BULLDOG: [
|
||||
"b12d08ea-c842-498a-82ea-889653588592",
|
||||
"78628e05-93ce-4f87-8a17-b910d32df51f",
|
||||
],
|
||||
LOCATION_ANCESTRAL_SMOOTHSNAKE: [
|
||||
"5680108a-19dc-4448-9344-3d0290217162",
|
||||
],
|
||||
LOCATION_EDGY_FOX: [
|
||||
"9d88605f-6871-46a8-bd46-9804ea04fca9",
|
||||
"ccdc7043-62af-44e8-a5fc-38b008c2044e",
|
||||
],
|
||||
LOCATION_WET_RAT: ["07ffa72a-bbac-45ca-8c9f-b9c1b526153a"],
|
||||
LOCATION_ELEGANT_LLAMA: [
|
||||
"72aaaa7b-4386-4ee7-9e9e-73fb8ff8e416",
|
||||
"1e4423b7-d4ff-448f-a8a8-4bb600cab7e3",
|
||||
"edbacf4b-e402-4548-b723-cd4351571537",
|
||||
],
|
||||
LOCATION_TRAPPED_WOLVERINE: ["078a50d1-6427-4fc3-9099-e46390e637a0"],
|
||||
},
|
||||
sniper: {
|
||||
LOCATION_AUSTRIA: ["ff9f46cf-00bd-4c12-b887-eac491c3a96d"],
|
||||
LOCATION_SALTY_SEAGULL: ["00e57709-e049-44c9-a2c3-7655e19884fb"],
|
||||
LOCATION_CAGED_FALCON: ["25b20d86-bb5a-4ebd-b6bb-81ed2779c180"],
|
||||
},
|
||||
elusive: {},
|
||||
sarajevo: {},
|
||||
/**
|
||||
* Special property for pro mode missions (2016 exclusive).
|
||||
* Mapping of location parent to pro mode contract ID, instead of the typical mission array.
|
||||
*/
|
||||
pro1: {
|
||||
LOCATION_PARENT_PARIS: "5ee4d771-6ab3-41fa-ab4f-04970d0ca327",
|
||||
LOCATION_PARENT_COASTALTOWN: "644d36bd-1f88-44f9-9fed-14a51e5e3f6b",
|
||||
LOCATION_PARENT_MARRAKECH: "7b2d5500-7853-4ad0-b68a-14be791cfba2",
|
||||
LOCATION_PARENT_BANGKOK: "ad5f9051-045d-4b8e-8a4d-d84429f467f8",
|
||||
LOCATION_PARENT_COLORADO: "69b58abc-6535-4092-9afe-c046b26303e6",
|
||||
LOCATION_PARENT_HOKKAIDO: "3d885714-fa9a-4438-9e0f-c58dbcaab8b8",
|
||||
},
|
||||
}
|
1093
components/controller.ts
Normal file
1093
components/controller.ts
Normal file
File diff suppressed because it is too large
Load Diff
248
components/databaseHandler.ts
Normal file
248
components/databaseHandler.ts
Normal file
@ -0,0 +1,248 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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 { readFile, writeFile } from "atomically"
|
||||
import { join } from "path"
|
||||
import type { ContractSession, GameVersion, UserProfile } from "./types/types"
|
||||
import { serializeSession, deserializeSession } from "./sessionSerialization"
|
||||
import { castUserProfile } from "./utils"
|
||||
import { getConfig } from "./configSwizzleManager"
|
||||
import { log, LogLevel } from "./loggingInterop"
|
||||
|
||||
/**
|
||||
* Container for functions that handle file read/writes,
|
||||
* which could otherwise break if writing partial data.
|
||||
*/
|
||||
class AsyncUserDataGuard {
|
||||
private readonly userData: Record<string, UserProfile> = {}
|
||||
private readonly dirtyProfiles: Set<string> = new Set()
|
||||
|
||||
getProfile(id: string): UserProfile {
|
||||
return this.userData[id]
|
||||
}
|
||||
|
||||
addLoadedProfile(id: string, profile: UserProfile): void {
|
||||
if (!this.userData[id]) {
|
||||
setInterval(() => {
|
||||
if (!this.dirtyProfiles.has(id)) {
|
||||
return
|
||||
}
|
||||
|
||||
this.dirtyProfiles.delete(id)
|
||||
|
||||
this.write(id)
|
||||
.then(() => undefined)
|
||||
.catch((e) => {
|
||||
log(
|
||||
LogLevel.ERROR,
|
||||
`Failed to write user profile ${id}: ${e}`,
|
||||
)
|
||||
})
|
||||
}, 3000).unref()
|
||||
}
|
||||
|
||||
this.userData[id] = profile
|
||||
// just in case
|
||||
this.dirtyProfiles.delete(id)
|
||||
}
|
||||
|
||||
markDirty(id: string): void {
|
||||
this.dirtyProfiles.add(id)
|
||||
}
|
||||
|
||||
private async write(versionedId: string): Promise<void> {
|
||||
let path
|
||||
|
||||
const [id, gameVersion] = versionedId.split(".")
|
||||
|
||||
if (["scpc", "h1", "h2"].includes(gameVersion)) {
|
||||
path = join("userdata", gameVersion, "users", `${id}.json`)
|
||||
} else {
|
||||
path = join("userdata", "users", `${id}.json`)
|
||||
}
|
||||
|
||||
await writeFile(path, JSON.stringify(this.getProfile(versionedId)))
|
||||
}
|
||||
}
|
||||
|
||||
const asyncGuard = new AsyncUserDataGuard()
|
||||
|
||||
/**
|
||||
* Gets a user's profile data.
|
||||
*
|
||||
* @param userId The user's ID.
|
||||
* @param gameVersion The game's version.
|
||||
* @returns The user's profile
|
||||
*/
|
||||
export function getUserData(
|
||||
userId: string,
|
||||
gameVersion: GameVersion,
|
||||
): UserProfile {
|
||||
const data = asyncGuard.getProfile(`${userId}.${gameVersion}`)
|
||||
|
||||
// we may not have this profile
|
||||
if (data?.Extensions?.gamepersistentdata?.PersistentBool) {
|
||||
data.Extensions.gamepersistentdata.PersistentBool = {
|
||||
...data.Extensions.gamepersistentdata.PersistentBool,
|
||||
...getConfig("PersistentBools", true),
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a user's profile data.
|
||||
*
|
||||
* @param userId The user's ID.
|
||||
* @param gameVersion The game's version.
|
||||
* @returns The raw JSON data.
|
||||
*/
|
||||
export async function loadUserData(
|
||||
userId: string,
|
||||
gameVersion: GameVersion,
|
||||
): Promise<UserProfile> {
|
||||
let path
|
||||
|
||||
if (["scpc", "h1", "h2"].includes(gameVersion)) {
|
||||
path = join("userdata", gameVersion, "users", `${userId}.json`)
|
||||
} else {
|
||||
path = join("userdata", "users", `${userId}.json`)
|
||||
}
|
||||
|
||||
const userProfile = castUserProfile(
|
||||
JSON.parse((await readFile(path)).toString()),
|
||||
)
|
||||
|
||||
asyncGuard.addLoadedProfile(`${userId}.${gameVersion}`, userProfile)
|
||||
return userProfile
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a user's profile as dirty.
|
||||
* Dirty profiles are automatically saved by a background thread every 3 seconds.
|
||||
*
|
||||
* @param userId The user's ID.
|
||||
* @param gameVersion The game's version.
|
||||
*/
|
||||
export function writeUserData(userId: string, gameVersion: GameVersion): void {
|
||||
asyncGuard.markDirty(`${userId}.${gameVersion}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a previously-non existent user profile.
|
||||
* This is used for creating new profiles.
|
||||
*
|
||||
* @param userId The user's ID.
|
||||
* @param userProfile
|
||||
* @param gameVersion The game's version.
|
||||
*/
|
||||
export function writeNewUserData(
|
||||
userId: string,
|
||||
userProfile: UserProfile,
|
||||
gameVersion: GameVersion,
|
||||
): void {
|
||||
asyncGuard.addLoadedProfile(`${userId}.${gameVersion}`, userProfile)
|
||||
asyncGuard.markDirty(`${userId}.${gameVersion}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value of an external provider binding.
|
||||
*
|
||||
* @param userId The user's ID.
|
||||
* @param externalFolder The folder where this provider's users are stored.
|
||||
* @param gameVersion The game's version.
|
||||
*/
|
||||
export async function getExternalUserData(
|
||||
userId: string,
|
||||
externalFolder: string,
|
||||
gameVersion: GameVersion,
|
||||
): Promise<string> {
|
||||
if (["scpc", "h1", "h2"].includes(gameVersion)) {
|
||||
return (
|
||||
await readFile(
|
||||
join("userdata", gameVersion, externalFolder, `${userId}.json`),
|
||||
)
|
||||
).toString()
|
||||
}
|
||||
|
||||
return (
|
||||
await readFile(join("userdata", externalFolder, `${userId}.json`))
|
||||
).toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the value of an external provider binding.
|
||||
*
|
||||
* @param userId The user's ID.
|
||||
* @param externalFolder The folder where this provider's users are stored.
|
||||
* @param userData The data to write to the binding.
|
||||
* @param gameVersion The game's version.
|
||||
*/
|
||||
export async function writeExternalUserData(
|
||||
userId: string,
|
||||
externalFolder: string,
|
||||
userData: string,
|
||||
gameVersion: GameVersion,
|
||||
): Promise<void> {
|
||||
if (["scpc", "h1", "h2"].includes(gameVersion)) {
|
||||
return await writeFile(
|
||||
join("userdata", gameVersion, externalFolder, `${userId}.json`),
|
||||
userData,
|
||||
)
|
||||
}
|
||||
|
||||
return await writeFile(
|
||||
join("userdata", externalFolder, `${userId}.json`),
|
||||
userData,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a contract session from the contractSessions folder.
|
||||
*
|
||||
* @param sessionId The ID of the session to load.
|
||||
* @returns The contract session.
|
||||
*/
|
||||
export async function getContractSession(
|
||||
sessionId: string,
|
||||
): Promise<ContractSession> {
|
||||
return deserializeSession(
|
||||
JSON.parse(
|
||||
(
|
||||
await readFile(join("contractSessions", `${sessionId}.json`))
|
||||
).toString(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a contract session to the contractsSessions folder.
|
||||
*
|
||||
* @param sessionId The session's ID.
|
||||
* @param session The contract session.
|
||||
*/
|
||||
export async function writeContractSession(
|
||||
sessionId: string,
|
||||
session: ContractSession,
|
||||
): Promise<void> {
|
||||
return await writeFile(
|
||||
join("contractSessions", `${sessionId}.json`),
|
||||
JSON.stringify(serializeSession(session)),
|
||||
)
|
||||
}
|
230
components/discord/client.ts
Normal file
230
components/discord/client.ts
Normal file
@ -0,0 +1,230 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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 EventEmitter from "events"
|
||||
import { clearTimeout, setTimeout } from "timers"
|
||||
import { IPCTransport } from "./ipc"
|
||||
import { randomUUID } from "crypto"
|
||||
|
||||
function subKey(event, args) {
|
||||
return `${event}${JSON.stringify(args)}`
|
||||
}
|
||||
|
||||
export interface PresenceButton {
|
||||
label: string
|
||||
url: string
|
||||
}
|
||||
|
||||
export interface Presence {
|
||||
state?: string
|
||||
details?: string
|
||||
startTimestamp?: number | Date
|
||||
endTimestamp?: number | Date
|
||||
largeImageKey?: string
|
||||
largeImageText?: string
|
||||
smallImageKey?: string
|
||||
smallImageText?: string
|
||||
instance?: boolean
|
||||
buttons?: PresenceButton[]
|
||||
timestamps?: {
|
||||
start?: number
|
||||
end?: number
|
||||
}
|
||||
assets?: {
|
||||
large_image?: string
|
||||
large_text?: string
|
||||
small_image?: string
|
||||
small_text?: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface ClientOptions {
|
||||
clientId: string
|
||||
}
|
||||
|
||||
export class RPCClient extends EventEmitter {
|
||||
clientId?: string | undefined = undefined
|
||||
user?: unknown | undefined = undefined
|
||||
private transport: IPCTransport
|
||||
private _expecting: Map<string, Promise<unknown> & PromiseConstructor> =
|
||||
new Map()
|
||||
private _subscriptions: Map<string, Promise<unknown>> = new Map()
|
||||
private _connectPromise: Promise<unknown> | undefined = undefined
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
|
||||
this.transport = new IPCTransport(this)
|
||||
this.transport.on("message", this._onRpcMessage.bind(this))
|
||||
}
|
||||
|
||||
connect(clientId: string): Promise<unknown> {
|
||||
if (this._connectPromise) {
|
||||
return this._connectPromise
|
||||
}
|
||||
|
||||
this._connectPromise = new Promise((resolve, reject) => {
|
||||
this.clientId = clientId
|
||||
const timeout = setTimeout(() => {
|
||||
if (this.transport.unavailable) {
|
||||
return
|
||||
}
|
||||
|
||||
reject(new Error("RPC_CONNECTION_TIMEOUT"))
|
||||
}, 10e3)
|
||||
timeout.unref()
|
||||
|
||||
this.once("connected", () => {
|
||||
clearTimeout(timeout)
|
||||
resolve(this)
|
||||
})
|
||||
|
||||
this.transport.once("close", () => {
|
||||
this._expecting.forEach((e) => {
|
||||
e.reject(new Error("connection closed"))
|
||||
})
|
||||
|
||||
this.emit("disconnected")
|
||||
reject(new Error("connection closed"))
|
||||
})
|
||||
|
||||
this.transport.connect().catch(reject)
|
||||
})
|
||||
|
||||
return this._connectPromise
|
||||
}
|
||||
|
||||
async login(options: ClientOptions): Promise<this> {
|
||||
const { clientId } = options
|
||||
await this.connect(clientId)
|
||||
this.emit("ready")
|
||||
return this
|
||||
}
|
||||
|
||||
request(
|
||||
cmd: string,
|
||||
args: { pid: number; activity?: Presence; instance?: boolean },
|
||||
evt?: undefined,
|
||||
): Promise<unknown> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const nonce = randomUUID()
|
||||
this.transport.send({ cmd, args, evt, nonce })
|
||||
// @ts-expect-error It's a partial promise.
|
||||
this._expecting.set(nonce, { resolve, reject })
|
||||
})
|
||||
}
|
||||
|
||||
_onRpcMessage(message): void {
|
||||
if (message.cmd === "DISPATCH" && message.evt === "READY") {
|
||||
if (message.data.user) {
|
||||
this.user = message.data.user
|
||||
}
|
||||
|
||||
this.emit("connected")
|
||||
return
|
||||
}
|
||||
|
||||
if (!this._expecting.has(message.nonce)) {
|
||||
const subid = subKey(message.evt, message.args)
|
||||
|
||||
if (!this._subscriptions.has(subid)) {
|
||||
return
|
||||
}
|
||||
|
||||
// @ts-expect-error Strange promise call.
|
||||
this._subscriptions.get(subid)(message.data)
|
||||
return
|
||||
}
|
||||
|
||||
const { resolve, reject } = this._expecting.get(
|
||||
message.nonce,
|
||||
) as PromiseConstructor
|
||||
|
||||
if (message.evt === "ERROR") {
|
||||
const e = new Error(message.data.message)
|
||||
// @ts-expect-error The Error object shouldn't have this.
|
||||
e.data = message.data
|
||||
reject(e)
|
||||
} else {
|
||||
resolve(message.data)
|
||||
}
|
||||
|
||||
this._expecting.delete(message.nonce)
|
||||
}
|
||||
|
||||
async setActivity(
|
||||
args: Presence = {},
|
||||
pid = process.pid,
|
||||
): Promise<unknown> {
|
||||
let timestamps
|
||||
let assets
|
||||
|
||||
if (args.startTimestamp || args.endTimestamp) {
|
||||
// eslint-disable-next-line prefer-const
|
||||
timestamps = {
|
||||
start: args.startTimestamp,
|
||||
end: args.endTimestamp,
|
||||
}
|
||||
|
||||
if (timestamps.start instanceof Date) {
|
||||
timestamps.start = Math.round(timestamps.start.getTime())
|
||||
}
|
||||
|
||||
if (timestamps.end instanceof Date) {
|
||||
timestamps.end = Math.round(timestamps.end.getTime())
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
args.largeImageKey ||
|
||||
args.largeImageText ||
|
||||
args.smallImageKey ||
|
||||
args.smallImageText
|
||||
) {
|
||||
// eslint-disable-next-line prefer-const
|
||||
assets = {
|
||||
large_image: args.largeImageKey,
|
||||
large_text: args.largeImageText,
|
||||
small_image: args.smallImageKey,
|
||||
small_text: args.smallImageText,
|
||||
}
|
||||
}
|
||||
|
||||
return await this.request("SET_ACTIVITY", {
|
||||
pid,
|
||||
activity: {
|
||||
state: args.state,
|
||||
details: args.details,
|
||||
timestamps,
|
||||
assets,
|
||||
buttons: args.buttons,
|
||||
instance: !!args.instance,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async clearActivity(pid = process.pid): Promise<Awaited<unknown>> {
|
||||
return await this.request("SET_ACTIVITY", {
|
||||
pid,
|
||||
})
|
||||
}
|
||||
|
||||
async destroy(): Promise<void> {
|
||||
await this.transport.close()
|
||||
}
|
||||
}
|
235
components/discord/ipc.ts
Normal file
235
components/discord/ipc.ts
Normal file
@ -0,0 +1,235 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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 net from "net"
|
||||
import EventEmitter from "events"
|
||||
import axios from "axios"
|
||||
import { randomUUID } from "crypto"
|
||||
import type { RPCClient } from "./client"
|
||||
import { log, LogLevel } from "../loggingInterop"
|
||||
|
||||
const enum OPCodes {
|
||||
HANDSHAKE = 0,
|
||||
FRAME = 1,
|
||||
CLOSE = 2,
|
||||
PING = 3,
|
||||
PONG = 4,
|
||||
}
|
||||
|
||||
function getIPCPath(id: number): string {
|
||||
if (process.platform === "win32") {
|
||||
return `\\\\?\\pipe\\discord-ipc-${id}`
|
||||
}
|
||||
|
||||
const {
|
||||
env: { XDG_RUNTIME_DIR, TMPDIR, TMP, TEMP },
|
||||
} = process
|
||||
const prefix = XDG_RUNTIME_DIR || TMPDIR || TMP || TEMP || "/tmp"
|
||||
return `${prefix.replace(/\/$/, "")}/discord-ipc-${id}`
|
||||
}
|
||||
|
||||
function getIPC(id = 0): Promise<net.Socket | undefined> {
|
||||
return new Promise((resolve) => {
|
||||
const path = getIPCPath(id)
|
||||
const onerror = (err: Error) => {
|
||||
if (id < 10) {
|
||||
resolve(getIPC(id + 1))
|
||||
return
|
||||
}
|
||||
|
||||
log(
|
||||
LogLevel.WARN,
|
||||
"Failed to connect to Discord for rich presence. Discord may not be running.",
|
||||
)
|
||||
|
||||
if (err.stack) {
|
||||
log(LogLevel.DEBUG, err.stack)
|
||||
} else {
|
||||
log(LogLevel.DEBUG, err.message)
|
||||
}
|
||||
|
||||
resolve(undefined)
|
||||
}
|
||||
|
||||
const sock = net.createConnection(path, () => {
|
||||
sock.removeListener("error", onerror)
|
||||
resolve(sock)
|
||||
})
|
||||
|
||||
sock.once("error", onerror)
|
||||
})
|
||||
}
|
||||
|
||||
async function findEndpoint(tries = 0): Promise<string> {
|
||||
if (tries > 30) {
|
||||
throw new Error("Could not find endpoint")
|
||||
}
|
||||
|
||||
const endpoint = `http://127.0.0.1:${6463 + (tries % 10)}`
|
||||
|
||||
try {
|
||||
const r = await axios(endpoint)
|
||||
|
||||
if (r.status === 404) {
|
||||
return endpoint
|
||||
}
|
||||
|
||||
return findEndpoint(tries + 1)
|
||||
} catch (e) {
|
||||
return findEndpoint(tries + 1)
|
||||
}
|
||||
}
|
||||
|
||||
function encode(op: number, data): Buffer {
|
||||
data = JSON.stringify(data)
|
||||
const len = Buffer.byteLength(data)
|
||||
const packet = Buffer.alloc(8 + len)
|
||||
packet.writeInt32LE(op, 0)
|
||||
packet.writeInt32LE(len, 4)
|
||||
packet.write(data, 8, len)
|
||||
return packet
|
||||
}
|
||||
|
||||
const working = {
|
||||
full: "",
|
||||
op: undefined,
|
||||
}
|
||||
|
||||
function decode(socket: net.Socket, callback): void {
|
||||
const packet = socket.read()
|
||||
|
||||
if (!packet) {
|
||||
return
|
||||
}
|
||||
|
||||
let { op } = working
|
||||
let raw: string
|
||||
|
||||
if (working.full === "") {
|
||||
op = working.op = packet.readInt32LE(0)
|
||||
const len = packet.readInt32LE(4)
|
||||
raw = packet.slice(8, len + 8)
|
||||
} else {
|
||||
raw = packet.toString()
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(working.full + raw)
|
||||
callback({ op, data }) // eslint-disable-line callback-return
|
||||
working.full = ""
|
||||
working.op = undefined
|
||||
} catch (err) {
|
||||
working.full += raw
|
||||
}
|
||||
|
||||
decode(socket, callback)
|
||||
}
|
||||
|
||||
export class IPCTransport extends EventEmitter {
|
||||
/**
|
||||
* This will only be true if the initial connection failed, to prevent unhandled promise rejections.
|
||||
*/
|
||||
public unavailable = false
|
||||
private socket: net.Socket | undefined = undefined
|
||||
|
||||
constructor(private readonly client: RPCClient) {
|
||||
super()
|
||||
}
|
||||
|
||||
async connect() {
|
||||
const socket = (this.socket = await getIPC())
|
||||
|
||||
if (!this.socket) {
|
||||
// failed to connect
|
||||
this.unavailable = true
|
||||
return
|
||||
}
|
||||
|
||||
socket!.on("close", this.onClose.bind(this))
|
||||
socket!.on("error", this.onClose.bind(this))
|
||||
this.emit("open")
|
||||
socket!.write(
|
||||
encode(OPCodes.HANDSHAKE, {
|
||||
v: 1,
|
||||
client_id: this.client.clientId,
|
||||
}),
|
||||
)
|
||||
socket!.pause()
|
||||
socket!.on("readable", () => {
|
||||
decode(socket!, ({ op, data }) => {
|
||||
switch (op) {
|
||||
case OPCodes.PING:
|
||||
this.send(data, OPCodes.PONG)
|
||||
break
|
||||
case OPCodes.FRAME:
|
||||
if (!data) {
|
||||
return
|
||||
}
|
||||
|
||||
if (data.cmd === "AUTHORIZE" && data.evt !== "ERROR") {
|
||||
findEndpoint()
|
||||
.then((endpoint) => {
|
||||
// @ts-expect-error Unexpected property.
|
||||
this.client.request.endpoint = endpoint
|
||||
return
|
||||
})
|
||||
.catch((e) => {
|
||||
this.client.emit("error", e)
|
||||
})
|
||||
}
|
||||
|
||||
this.emit("message", data)
|
||||
break
|
||||
case OPCodes.CLOSE:
|
||||
this.emit("close", data)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
onClose(e): void {
|
||||
this.emit("close", e)
|
||||
}
|
||||
|
||||
send(data, op = OPCodes.FRAME): void {
|
||||
if (this.unavailable) {
|
||||
log(
|
||||
LogLevel.DEBUG,
|
||||
"Skipping RPC data send: transport unavailable.",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
this.socket?.write(encode(op, data))
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
this.once("close", resolve)
|
||||
this.send({}, OPCodes.CLOSE)
|
||||
this.socket?.end()
|
||||
})
|
||||
}
|
||||
|
||||
ping(): void {
|
||||
this.send(randomUUID(), OPCodes.PING)
|
||||
}
|
||||
}
|
379
components/discordRp.ts
Normal file
379
components/discordRp.ts
Normal file
@ -0,0 +1,379 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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 { RPCClient } from "./discord/client"
|
||||
import { getConfig } from "./configSwizzleManager"
|
||||
import type { GameVersion, MissionType } from "./types/types"
|
||||
import { getFlag } from "./flags"
|
||||
import { log, LogLevel } from "./loggingInterop"
|
||||
|
||||
let rpcClient: undefined | RPCClient
|
||||
/*@__NOINLINE__*/
|
||||
const processStartTime = Math.round(Date.now() / 1000)
|
||||
|
||||
export function initRp(): void {
|
||||
Object.keys(getConfig("Entrances", false)).forEach((key) => {
|
||||
if (!scenePathToRpAsset(key, []) && PEACOCK_DEV) {
|
||||
log(LogLevel.DEBUG, `WARNING missing scene ${key} for RP!`)
|
||||
}
|
||||
})
|
||||
|
||||
// creates a new rp client, pretty self-explanatory.
|
||||
rpcClient = new RPCClient()
|
||||
|
||||
// connects to the Peacock discord developer app, which contains the images for the rp.
|
||||
rpcClient.login({
|
||||
clientId: "846361353027584013",
|
||||
})
|
||||
|
||||
// checks if the rp is on the state "ready", and starts the rp, displaying the details on discord.
|
||||
rpcClient.on("ready", () => {
|
||||
swapToIdle()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the user's Discord RP status to "browsing menus" if the feature is enabled.
|
||||
*
|
||||
* @param gameVersion The game version.
|
||||
*/
|
||||
export function swapToBrowsingMenusStatus(gameVersion: GameVersion): void {
|
||||
if (getFlag("discordRp") === true) {
|
||||
rpcClient?.setActivity({
|
||||
details: `Playing HITMAN™ ${gameVersion
|
||||
.substring(1)
|
||||
.replace("1", "(2016)")}`,
|
||||
largeImageKey: gameVersion,
|
||||
largeImageText: "Browsing Menus",
|
||||
smallImageKey: "peacock",
|
||||
smallImageText: "Using Peacock",
|
||||
startTimestamp:
|
||||
getFlag("discordRpAppTime") === true
|
||||
? processStartTime
|
||||
: Math.round(Date.now() / 1000),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the user's Discord RP status to "idle" if the feature is enabled.
|
||||
*/
|
||||
export function swapToIdle(): void {
|
||||
if (getFlag("discordRp") === true) {
|
||||
rpcClient?.setActivity({
|
||||
details: "Idle",
|
||||
largeImageKey: "peacock",
|
||||
largeImageText: "Idling",
|
||||
startTimestamp:
|
||||
getFlag("discordRpAppTime") === true
|
||||
? processStartTime
|
||||
: Math.round(Date.now() / 1000),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the user's Discord RP status to playing something if the feature is enabled.
|
||||
*
|
||||
* @param scenePath The path to the loaded scene, used to determine names/assets used.
|
||||
* @param missionType The mission's type.
|
||||
* @param bricks The mission's loaded bricks.
|
||||
*/
|
||||
export function swapToLocationStatus(
|
||||
scenePath: string,
|
||||
missionType: MissionType,
|
||||
bricks: string[],
|
||||
): void {
|
||||
if (getFlag("discordRp") !== true) {
|
||||
return
|
||||
}
|
||||
|
||||
const details = scenePathToRpAsset(scenePath, bricks)
|
||||
|
||||
if (!Array.isArray(details)) {
|
||||
// we need to handle this somehow in the future
|
||||
return
|
||||
}
|
||||
|
||||
const formattedMissionType =
|
||||
missionType === "orbis"
|
||||
? "Sarajevo Six"
|
||||
: `${missionType
|
||||
.substring(0, 1)
|
||||
.toUpperCase()}${missionType.substring(1)}`
|
||||
|
||||
rpcClient?.setActivity({
|
||||
state: `${formattedMissionType} in ${details[2]}`,
|
||||
details: details[1],
|
||||
largeImageKey: details[0],
|
||||
largeImageText: details[1],
|
||||
smallImageKey: "peacock",
|
||||
smallImageText: "Using Peacock",
|
||||
startTimestamp:
|
||||
getFlag("discordRpAppTime") === true
|
||||
? processStartTime
|
||||
: Math.round(Date.now() / 1000),
|
||||
})
|
||||
}
|
||||
|
||||
type BrickDataMap = Record<string, [string, string, string]>
|
||||
|
||||
/**
|
||||
* Translates a scene path to a Discord RP asset and human-readable name.
|
||||
*
|
||||
* @param scenePath The scene path.
|
||||
* @param bricks The mission's loaded bricks.
|
||||
* @returns An array of the RP asset, human-readable name, and location human-readable name.
|
||||
*/
|
||||
export function scenePathToRpAsset(
|
||||
scenePath: string,
|
||||
bricks: string[],
|
||||
): string[] | undefined {
|
||||
const brickAssetsMap = getConfig<BrickDataMap>(
|
||||
"DiscordRichAssetsForBricks",
|
||||
false,
|
||||
)
|
||||
|
||||
for (const brick of bricks) {
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(
|
||||
brickAssetsMap,
|
||||
brick.toLowerCase(),
|
||||
)
|
||||
) {
|
||||
return brickAssetsMap[brick.toLowerCase()]!
|
||||
}
|
||||
}
|
||||
|
||||
switch (scenePath.toLowerCase()) {
|
||||
// paris
|
||||
case "assembly:/_pro/scenes/missions/paris/_scene_paris.entity":
|
||||
case "assembly:/_pro/scenes/missions/paris/_scene_paris_torenia.entity":
|
||||
case "assembly:/_pro/scenes/missions/paris/_scene_fashionshowhit_01.entity":
|
||||
case "assembly:/_pro/scenes/missions/paris/_scene_tutorial_contractcreationparis.entity":
|
||||
case "assembly:/_pro/scenes/missions/paris/_scene_fashionshowhit_amd.entity":
|
||||
return ["parisshowstopper", "The Showstopper", "Paris"]
|
||||
case "assembly:/_pro/scenes/missions/paris/_scene_tequilasunrise_01.entity":
|
||||
return ["elusivetequilasunrise", "The Forger", "Paris"]
|
||||
case "assembly:/_pro/scenes/missions/paris/_scene_whiterussian_01.entity":
|
||||
return ["elusivewhiterussian", "The Identity Thief", "Paris"]
|
||||
|
||||
// sapienza
|
||||
case "assembly:/_pro/scenes/missions/coastaltown/_scene_copperhead.entity":
|
||||
case "assembly:/_pro/scenes/missions/coastaltown/_scene_mission_copperhead.entity":
|
||||
return ["sapienzaicon", "The Icon", "Sapienza"]
|
||||
case "assembly:/_pro/scenes/missions/coastaltown/_scene_mamba.entity":
|
||||
case "assembly:/_pro/scenes/missions/coastaltown/_scene_mission_mamba.entity":
|
||||
return ["sapienzalandslide", "Landslide", "Sapienza"]
|
||||
case "assembly:/_pro/scenes/missions/coastaltown/mission01.entity":
|
||||
case "assembly:/_pro/scenes/missions/coastaltown/_scene_octopus.entity":
|
||||
return ["sapienzaworldoftomorrow", "World of Tomorrow", "Sapienza"]
|
||||
case "assembly:/_pro/scenes/missions/coastaltown/scene_ebola.entity":
|
||||
return ["sapienzapzauthor", "The Author", "Sapienza"]
|
||||
|
||||
// marrakesh
|
||||
case "assembly:/_pro/scenes/missions/marrakesh/_scene_mission_spider.entity":
|
||||
case "assembly:/_pro/scenes/missions/marrakesh/_scene_spider.entity":
|
||||
return ["marrakeshguildedcage", "A Gilded Cage", "Marrakesh"]
|
||||
case "assembly:/_pro/scenes/missions/marrakesh/_scene_mission_python.entity":
|
||||
case "assembly:/_pro/scenes/missions/marrakesh/_scene_python_hellebore.entity":
|
||||
case "assembly:/_pro/scenes/missions/marrakesh/_scene_python.entity":
|
||||
return ["marrakeshahbos", "A House Built On Sand", "Marrakesh"]
|
||||
|
||||
// bangkok missions
|
||||
case "assembly:/_pro/scenes/missions/bangkok/_scene_mission_tiger.entity":
|
||||
case "assembly:/_pro/scenes/missions/bangkok/_scene_tiger.entity":
|
||||
return ["bangkokclub27", "Club 27", "Bangkok"]
|
||||
case "assembly:/_pro/scenes/missions/bangkok/scene_zika.entity":
|
||||
return ["bangkokpzsource", "The Source", "Bangkok"]
|
||||
|
||||
// colorado missions
|
||||
case "assembly:/_pro/scenes/missions/colorado_2/_scene_bull.entity":
|
||||
case "assembly:/_pro/scenes/missions/colorado_2/_scene_mission_bull.entity":
|
||||
return ["coloradofreedomfighters", "Freedom Fighters", "Colorado"]
|
||||
case "assembly:/_pro/scenes/missions/colorado_2/scene_rabies.entity":
|
||||
return ["coloradopzvector", "The Vector", "Colorado"]
|
||||
|
||||
// hokkaido missions
|
||||
case "assembly:/_pro/scenes/missions/hokkaido/_scene_mission_snowcrane.entity":
|
||||
case "assembly:/_pro/scenes/missions/hokkaido/_scene_snowcrane_tumbleweed.entity":
|
||||
case "assembly:/_pro/scenes/missions/hokkaido/_scene_snowcrane.entity":
|
||||
return ["hokkaidositusinvertus", "Situs Invertus", "Hokkaido"]
|
||||
case "assembly:/_pro/scenes/missions/hokkaido/scene_mamushi.entity":
|
||||
return [
|
||||
"hokkaidosnowfestival",
|
||||
"Hokkaido Snow Festival",
|
||||
"Hokkaido",
|
||||
]
|
||||
case "assembly:/_pro/scenes/missions/hokkaido/_scene_flu.entity":
|
||||
return ["hokkaidopz", "Patient Zero", "Hokkaido"]
|
||||
|
||||
// dartmoor
|
||||
case "assembly:/_pro/scenes/missions/ancestral/scene_bulldog.entity":
|
||||
return ["dartmoordeathoffamily", "Death in the Family", "Dartmoor"]
|
||||
case "assembly:/_pro/scenes/missions/ancestral/scene_smoothsnake.entity":
|
||||
return ["dartmoorgardenshow", "Dartmoor Garden Show", "Dartmoor"]
|
||||
case "assembly:/_pro/scenes/missions/ancestral/scene_ancestral_vesper.entity":
|
||||
return ["elusivevesper", "The Procurers", "Dartmoor"]
|
||||
|
||||
// columbia
|
||||
case "assembly:/_pro/scenes/missions/colombia/mission_millipede/scene_millipede.entity":
|
||||
case "assembly:/_pro/scenes/missions/colombia/scene_anaconda.entity":
|
||||
return [
|
||||
"santafortunaembraceserpent",
|
||||
"Embrace of the Serpent",
|
||||
"Santa Fortuna",
|
||||
]
|
||||
case "assembly:/_pro/scenes/missions/colombia/scene_hippo.entity":
|
||||
case "assembly:/_pro/scenes/missions/colombia/scene_hippo_calluna.entity":
|
||||
case "assembly:/_pro/scenes/missions/colombia/scene_hippo_rafflesia.entity":
|
||||
return [
|
||||
"santafortunathreeheadedserpent",
|
||||
"Three-Headed Serpent",
|
||||
"Santa Fortuna",
|
||||
]
|
||||
|
||||
// berlin
|
||||
case "assembly:/_pro/scenes/missions/edgy/mission_fox/scene_radler.entity":
|
||||
return ["elusiveradler", "The Liability", "Berlin"]
|
||||
case "assembly:/_pro/scenes/missions/edgy/mission_fox/scene_fox_basic.entity":
|
||||
case "assembly:/_pro/scenes/missions/edgy/mission_fox/scene_fox_contractcreation.entity":
|
||||
case "assembly:/_pro/scenes/missions/edgy/mission_fox/scene_fox_cornflower.entity":
|
||||
case "assembly:/_pro/scenes/missions/edgy/mission_fox/scene_fox_nightphlox.entity":
|
||||
case "assembly:/_pro/scenes/missions/edgy/mission_fox/scene_fox_smilax.entity":
|
||||
case "assembly:/_pro/scenes/missions/edgy/mission_fox/scene_fox_smilax_level2.entity":
|
||||
case "assembly:/_pro/scenes/missions/edgy/mission_fox/scene_fox_smilax_level3.entity":
|
||||
return ["berlinapexpredator", "Apex Predator", "Berlin"]
|
||||
case "assembly:/_pro/scenes/missions/edgy/mission_fox/scene_grasssnake.entity":
|
||||
return ["berlinegghunt", "Berlin Egg Hunt", "Berlin"]
|
||||
|
||||
// mendozer
|
||||
case "assembly:/_pro/scenes/missions/elegant/scene_llama_elusive_clerico.entity":
|
||||
return ["elusiveclerico", "The Heartbreaker", "Mendoza"]
|
||||
case "assembly:/_pro/scenes/missions/elegant/scene_llama_elusive_jockeyclub.entity":
|
||||
return ["elusivejockeyclub", "The Iconoclast", "Mendoza"]
|
||||
case "assembly:/_pro/scenes/missions/elegant/scene_llama.entity":
|
||||
case "assembly:/_pro/scenes/missions/elegant/scene_llama_jacaranda.entity":
|
||||
case "assembly:/_pro/scenes/missions/elegant/scene_whitedryas.entity":
|
||||
case "assembly:/_pro/scenes/missions/elegant/scene_whitedryas_level2.entity":
|
||||
case "assembly:/_pro/scenes/missions/elegant/scene_whitedryas_level3.entity":
|
||||
return ["mendozafarewell", "The Farewell", "Mendoza"]
|
||||
|
||||
// dubai
|
||||
case "assembly:/_pro/scenes/missions/golden/mission_gecko/scene_gecko_angelica.entity":
|
||||
case "assembly:/_pro/scenes/missions/golden/mission_gecko/scene_gecko_basic.entity":
|
||||
case "assembly:/_pro/scenes/missions/golden/mission_gecko/scene_gecko_desertrose.entity":
|
||||
case "assembly:/_pro/scenes/missions/golden/mission_gecko/scene_gecko_lunaria.entity":
|
||||
case "assembly:/_pro/scenes/missions/golden/mission_gecko/scene_gecko_sinstest.entity":
|
||||
case "assembly:/_pro/scenes/missions/golden/mission_gecko/scene_gecko_sheepsorrel.entity":
|
||||
return ["dubaiontopoftheworld", "On Top Of The World", "Dubai"]
|
||||
case "assembly:/_pro/scenes/missions/golden/mission_gecko/scene_gibson.entity":
|
||||
return ["elusivegibson", "The Ascensionist", "Dubai"]
|
||||
|
||||
// chongqing
|
||||
case "assembly:/_pro/scenes/missions/wet/scene_wet_makoyana.entity":
|
||||
case "assembly:/_pro/scenes/missions/wet/scene_rat_ginseng.entity":
|
||||
case "assembly:/_pro/scenes/missions/wet/scene_rat_basic.entity":
|
||||
case "assembly:/_pro/scenes/missions/wet/scene_magnolia.entity":
|
||||
return ["chongqingendofanera", "End Of An Era", "Chongqing"]
|
||||
case "assembly:/_pro/scenes/missions/wet/scene_rat_elusive_redsnapper.entity":
|
||||
return ["elusiveredsnapper", "The Rage", "Chongqing"]
|
||||
|
||||
// training
|
||||
case "assembly:/_pro/scenes/missions/thefacility/_scene_polarbear_005.entity":
|
||||
case "assembly:/_pro/scenes/missions/thefacility/_scene_mission_polarbear_002_for_escalation_.entity":
|
||||
case "assembly:/_pro/scenes/missions/thefacility/_scene_mission_polarbear_002_contracts_creation_tutorial.entity":
|
||||
case "assembly:/_pro/scenes/missions/thefacility/_scene_mission_polarbear_intro_firsttime.entity":
|
||||
case "assembly:/_pro/scenes/missions/thefacility/_scene_mission_polarbear_module_002.entity":
|
||||
case "assembly:/_pro/scenes/missions/thefacility/_scene_mission_polarbear_module_002_b.entity":
|
||||
case "assembly:/_pro/scenes/missions/thefacility/_scene_mission_polarbear_module_005.entity":
|
||||
return ["icafacilityfinaltest", "ICA Facility", "Greenland"]
|
||||
|
||||
// new york
|
||||
case "assembly:/_pro/scenes/missions/greedy/mission_raccoon/scene_raccoon_basic.entity":
|
||||
case "assembly:/_pro/scenes/missions/greedy/mission_raccoon/scene_raccoon_basic_dandelion.entity":
|
||||
return ["newyorkgoldenhandshake", "Golden Handshake", "New York"]
|
||||
|
||||
// haven
|
||||
case "assembly:/_pro/scenes/missions/opulent/mission_stingray/scene_stingray_basic.entity":
|
||||
case "assembly:/_pro/scenes/missions/opulent/mission_stingray/scene_stingray_arcticthyme.entity":
|
||||
return [
|
||||
"havenlastresort",
|
||||
"The Last Resort",
|
||||
"Haven - The Maldives",
|
||||
]
|
||||
|
||||
// miami
|
||||
case "assembly:/_pro/scenes/missions/miami/scene_et_sambuca.entity":
|
||||
return ["elusivesambuca", "The Undying", "Miami"]
|
||||
case "assembly:/_pro/scenes/missions/miami/scene_flamingo.entity":
|
||||
return ["miamifinishline", "The Finish Line", "Miami"]
|
||||
case "assembly:/_pro/scenes/missions/miami/scene_cottonmouth.entity":
|
||||
return ["miamisilvertounge", "A Silver Tongue", "Miami"]
|
||||
|
||||
// hawkes bay
|
||||
case "assembly:/_pro/scenes/missions/sheep/scene_adonis.entity":
|
||||
return ["elusiveadonis", "The Politician", "New Zealand"]
|
||||
case "assembly:/_pro/scenes/missions/sheep/scene_sheep.entity":
|
||||
return ["hawkenightcall", "Nightcall", "New Zealand"]
|
||||
case "assembly:/_pro/scenes/missions/sheep/scene_opuntia.entity":
|
||||
return ["opuntia", "Opuntia", "New Zealand"]
|
||||
|
||||
// sgail
|
||||
case "assembly:/_pro/scenes/missions/theark/scene_magpie.entity":
|
||||
case "assembly:/_pro/scenes/missions/theark/_scene_magpie_pansy.entity":
|
||||
case "assembly:/_pro/scenes/missions/theark/_scene_magpie_lotus.entity":
|
||||
return ["sgailarksociety", "The Ark Society", "Isle of Sgáil"]
|
||||
|
||||
// whittleton
|
||||
case "assembly:/_pro/scenes/missions/skunk/scene_skunk.entity":
|
||||
case "assembly:/_pro/scenes/missions/skunk/mission_grasshopper/scene_grasshopper.entity":
|
||||
return ["whittletonanotherlife", "Another Life", "Whittleton Creek"]
|
||||
case "assembly:/_pro/scenes/missions/skunk/scene_gartersnake.entity":
|
||||
return ["whittletonbitterpill", "A Bitter Pill", "Whittleton Creek"]
|
||||
|
||||
// mumbai
|
||||
case "assembly:/_pro/scenes/missions/mumbai/scene_mongoose.entity":
|
||||
return ["mumbaichasingghost", "Chasing A Ghost", "Mumbai"]
|
||||
case "assembly:/_pro/scenes/missions/mumbai/scene_kingcobra.entity":
|
||||
return ["mumbaikingcobra", "Illusions of Grandeur", "Mumbai"]
|
||||
|
||||
// romania
|
||||
case "assembly:/_pro/scenes/missions/trapped/scene_bellflower.entity":
|
||||
case "assembly:/_pro/scenes/missions/trapped/scene_wolverine.entity":
|
||||
return ["romaniatile", "Untouchable", "Romania"]
|
||||
|
||||
// ambrose
|
||||
case "assembly:/_pro/scenes/missions/rocky/scene_dugong.entity":
|
||||
return [
|
||||
"shadowsinthewater",
|
||||
"Shadows in the Water",
|
||||
"Ambrose Island",
|
||||
]
|
||||
|
||||
// sniper
|
||||
case "assembly:/_pro/scenes/missions/caged/mission_falcon/scene_falcon_sniper.entity":
|
||||
return ["sniper_siberia", "Crime and Punishment", "Siberia"]
|
||||
case "assembly:/_pro/scenes/missions/hawk/scene_hawk.entity":
|
||||
return ["austria", "The Last Yardbird", "Austria"]
|
||||
case "assembly:/_pro/scenes/missions/salty/mission_seagull/scene_seagull.entity":
|
||||
return ["hantuport", "The Pen and the Sword", "Hantu Port"]
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
155
components/entitlementStrategies.ts
Normal file
155
components/entitlementStrategies.ts
Normal file
@ -0,0 +1,155 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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 { log, LogLevel } from "./loggingInterop"
|
||||
import { userAuths } from "./officialServerAuth"
|
||||
import {
|
||||
EPIC_NAMESPACE_2021,
|
||||
FRANKENSTEIN_SNIPER_ENTITLEMENTS,
|
||||
getEpicEntitlements,
|
||||
H2_STEAM_ENTITLEMENTS,
|
||||
STEAM_NAMESPACE_2016,
|
||||
} from "./platformEntitlements"
|
||||
import { GameVersion } from "./types/types"
|
||||
|
||||
/**
|
||||
* The base class for an entitlement strategy.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
abstract class EntitlementStrategy {
|
||||
abstract get(
|
||||
accessToken: string,
|
||||
userId: string,
|
||||
): string[] | Promise<string[]>
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider for HITMAN 3 on Epic Games Store.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export class EpicH3Strategy extends EntitlementStrategy {
|
||||
override async get(accessToken: string, userId: string) {
|
||||
return await getEpicEntitlements(
|
||||
EPIC_NAMESPACE_2021,
|
||||
userId,
|
||||
accessToken,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider for any game using the official servers.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export class IOIStrategy extends EntitlementStrategy {
|
||||
private readonly _remoteService: string
|
||||
|
||||
constructor(gameVersion: GameVersion, private readonly issuerId: string) {
|
||||
super()
|
||||
this.issuerId = issuerId
|
||||
this._remoteService =
|
||||
gameVersion === "h3"
|
||||
? "hm3-service"
|
||||
: gameVersion === "h2"
|
||||
? "pc2-service"
|
||||
: "pc-service"
|
||||
}
|
||||
|
||||
override async get(userId: string) {
|
||||
if (!userAuths.has(userId)) {
|
||||
log(LogLevel.ERROR, `No data found for ${userId}.`)
|
||||
return []
|
||||
}
|
||||
|
||||
const user = userAuths.get(userId)
|
||||
|
||||
const resp = await user?._useService<string[]>(
|
||||
`https://${this._remoteService}.hitman.io/authentication/api/userchannel/ProfileService/GetPlatformEntitlements`,
|
||||
false,
|
||||
{
|
||||
issuerId: this.issuerId,
|
||||
},
|
||||
)
|
||||
|
||||
return resp?.data || []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider for HITMAN 2016 on Epic Games.
|
||||
* TODO: This does not work due to old Epic API version. We should probably not hard-code this!
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export class EpicH1Strategy extends EntitlementStrategy {
|
||||
override get() {
|
||||
return [
|
||||
"0a73eaedcac84bd28b567dbec764c5cb", // Hitman 1 standard edition
|
||||
"81aecb49a60b47478e61e1cbd68d63c5", // Hitman 1 GOTY upgrade
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider for HITMAN: Sniper Challenge (Hawk) on Steam.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export class SteamScpcStrategy extends EntitlementStrategy {
|
||||
override get() {
|
||||
return FRANKENSTEIN_SNIPER_ENTITLEMENTS
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider for HITMAN 2016 on Steam.
|
||||
* TODO: This does not work because of the Steam API. We should probably not hard-code this!
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export class SteamH1Strategy extends EntitlementStrategy {
|
||||
override get() {
|
||||
return [
|
||||
STEAM_NAMESPACE_2016,
|
||||
"439870",
|
||||
"439890",
|
||||
"440930",
|
||||
"440940",
|
||||
"440960",
|
||||
"440961",
|
||||
"440962",
|
||||
"505180",
|
||||
"588780",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider for HITMAN 2 on Steam.
|
||||
* TODO: This does not work because of the Steam API. We should probably not hard-code this!
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export class SteamH2Strategy extends EntitlementStrategy {
|
||||
override get() {
|
||||
return H2_STEAM_ENTITLEMENTS
|
||||
}
|
||||
}
|
818
components/eventHandler.ts
Normal file
818
components/eventHandler.ts
Normal file
@ -0,0 +1,818 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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 { Router } from "express"
|
||||
import {
|
||||
ClientToServerEvent,
|
||||
ContractSession,
|
||||
GameVersion,
|
||||
MissionManifestObjective,
|
||||
PeacockCameraStatus,
|
||||
PushMessage,
|
||||
RatingKill,
|
||||
RequestWithJwt,
|
||||
S2CEventWithTimestamp,
|
||||
Seconds,
|
||||
ServerToClientEvent,
|
||||
} from "./types/types"
|
||||
import { extractToken, ServerVer } from "./utils"
|
||||
import { json as jsonMiddleware } from "body-parser"
|
||||
import { log, LogLevel } from "./loggingInterop"
|
||||
import { getContractSession, writeContractSession } from "./databaseHandler"
|
||||
import { controller } from "./controller"
|
||||
import { swapToLocationStatus } from "./discordRp"
|
||||
import { randomUUID } from "crypto"
|
||||
import { liveSplitManager } from "./livesplit/liveSplitManager"
|
||||
import { handleMultiplayerEvent } from "./multiplayer/multiplayerService"
|
||||
import { handleEvent } from "@peacockproject/statemachine-parser"
|
||||
import picocolors from "picocolors"
|
||||
import { encodePushMessage } from "./multiplayer/multiplayerUtils"
|
||||
import {
|
||||
ActorTaggedC2SEvent,
|
||||
AmbientChangedC2SEvent,
|
||||
BodyHiddenC2SEvent,
|
||||
ContractStartC2SEvent,
|
||||
HeroSpawn_LocationC2SEvent,
|
||||
ItemDroppedC2SEvent,
|
||||
ItemPickedUpC2SEvent,
|
||||
KillC2SEvent,
|
||||
MurderedBodySeenC2SEvent,
|
||||
ObjectiveCompletedC2SEvent,
|
||||
PacifyC2SEvent,
|
||||
SecuritySystemRecorderC2SEvent,
|
||||
SetpiecesC2SEvent,
|
||||
SpottedC2SEvent,
|
||||
WitnessesC2SEvent,
|
||||
} from "./types/events"
|
||||
|
||||
const eventRouter = Router()
|
||||
|
||||
// /authentication/api/userchannel/EventsService/
|
||||
|
||||
const eventQueue = new Map<string, S2CEventWithTimestamp[]>()
|
||||
const pushMessageQueue = new Map<string, PushMessage[]>()
|
||||
|
||||
/**
|
||||
* Enqueue a server to client push message.
|
||||
* It will be sent back the next time the client calls `SaveAndSynchronizeEvents4`.
|
||||
*
|
||||
* @param userId The push message's target user.
|
||||
* @param message The encoded push message to send.
|
||||
* @see enqueueEvent
|
||||
* @author grappigegovert
|
||||
*/
|
||||
export function enqueuePushMessage(userId: string, message: unknown): void {
|
||||
let userQueue
|
||||
const time = process.hrtime.bigint()
|
||||
|
||||
if ((userQueue = pushMessageQueue.get(userId))) {
|
||||
userQueue.push({
|
||||
time,
|
||||
message: encodePushMessage(time, message),
|
||||
})
|
||||
} else {
|
||||
userQueue = [
|
||||
{
|
||||
time,
|
||||
message: encodePushMessage(time, message),
|
||||
},
|
||||
]
|
||||
pushMessageQueue.set(userId, userQueue)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a listener for an objective. Allows server-side tracking of the objective's state as events come in.
|
||||
*
|
||||
* @param session The contract session.
|
||||
* @param objective The objective object.
|
||||
* @author Reece Dunham
|
||||
*/
|
||||
export function registerObjectiveListener(
|
||||
session: ContractSession,
|
||||
objective: MissionManifestObjective,
|
||||
): void {
|
||||
if (!objective.Definition) {
|
||||
return
|
||||
}
|
||||
|
||||
let context = objective.Definition.Context || {}
|
||||
let state = "Start"
|
||||
|
||||
session.objectiveDefinitions.set(objective.Id, objective.Definition)
|
||||
|
||||
const immediate = handleEvent(
|
||||
// @ts-expect-error Type issue, needs to be corrected in sm-p.
|
||||
objective.Definition,
|
||||
context,
|
||||
{},
|
||||
{
|
||||
eventName: "-",
|
||||
currentState: state,
|
||||
},
|
||||
)
|
||||
|
||||
if (immediate.state) {
|
||||
state = immediate.state
|
||||
}
|
||||
|
||||
if (immediate.context) {
|
||||
context = immediate.context
|
||||
}
|
||||
|
||||
session.objectiveContexts.set(objective.Id, context)
|
||||
session.objectiveStates.set(objective.Id, state)
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue a server to client event.
|
||||
* It will be sent back the next time the client calls `SaveAndSynchronizeEvents4`.
|
||||
*
|
||||
* @param userId The event's target user.
|
||||
* @param event The event to send.
|
||||
* @see enqueuePushMessage
|
||||
* @author grappigegovert
|
||||
*/
|
||||
export function enqueueEvent(userId: string, event: ServerToClientEvent): void {
|
||||
let userQueue: S2CEventWithTimestamp[] | undefined
|
||||
const time = process.hrtime.bigint().toString()
|
||||
event.CreatedAt = new Date().toISOString().slice(0, -1)
|
||||
event.Token = time.toString()
|
||||
|
||||
if ((userQueue = eventQueue.get(userId))) {
|
||||
userQueue.push({
|
||||
time,
|
||||
event,
|
||||
})
|
||||
} else {
|
||||
userQueue = [
|
||||
{
|
||||
time,
|
||||
event,
|
||||
},
|
||||
]
|
||||
eventQueue.set(userId, userQueue)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Like the game's internal enum for EDeathContext.
|
||||
*
|
||||
* @see https://github.com/OrfeasZ/ZHMModSDK/blob/ba9512092a37d3b1f4de047bdd6acf15a7b9ac7c/ZHMModSDK/Include/Glacier/Enums.h#L6158
|
||||
*/
|
||||
export const enum EDeathContext {
|
||||
eDC_UNDEFINED = 0,
|
||||
eDC_NOT_HERO = 1,
|
||||
eDC_HIDDEN = 2,
|
||||
eDC_ACCIDENT = 3,
|
||||
eDC_MURDER = 4,
|
||||
}
|
||||
|
||||
export const contractSessions = new Map<string, ContractSession>()
|
||||
const userIdToTempSession = new Map<string, string>()
|
||||
|
||||
/**
|
||||
* Get the current state of an objective.
|
||||
*
|
||||
* @param sessionId The session ID.
|
||||
* @param objectiveId The objective ID.
|
||||
*/
|
||||
export function getCurrentState(
|
||||
sessionId: string,
|
||||
objectiveId: string,
|
||||
): string | undefined {
|
||||
// Note: after the double-layered maps are merged into the session object, this should be rewritten.
|
||||
const session = contractSessions.get(sessionId)
|
||||
|
||||
if (!session) {
|
||||
return "Start"
|
||||
}
|
||||
|
||||
return session.objectiveStates.get(objectiveId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new contract session.
|
||||
*
|
||||
* @param sessionId The ID for the session.
|
||||
* @param contractId The ID of the contract the session is for.
|
||||
* @param userId The ID of the user playing the session.
|
||||
* @param difficulty The difficulty of the game.
|
||||
* @param gameVersion The game version.
|
||||
* @param doScoring If true, this will be treated like a normal session. If false, this session will not be scored/put on the leaderboards. This should be false if we don't have full session details, e.g. if this is a save from the official servers loaded on Peacock.
|
||||
*/
|
||||
export function newSession(
|
||||
sessionId: string,
|
||||
contractId: string,
|
||||
userId: string,
|
||||
difficulty: number,
|
||||
gameVersion: GameVersion,
|
||||
doScoring = true,
|
||||
): void {
|
||||
const timestamp = new Date()
|
||||
|
||||
const contract = controller.resolveContract(contractId)
|
||||
|
||||
if (!contract) {
|
||||
log(LogLevel.ERROR, `Failed to load ${contractId}`)
|
||||
throw new Error("no ct")
|
||||
}
|
||||
|
||||
swapToLocationStatus(
|
||||
contract.Metadata.ScenePath,
|
||||
contract.Metadata.Type,
|
||||
contract.Data.Bricks || [],
|
||||
)
|
||||
|
||||
contractSessions.set(sessionId, {
|
||||
gameVersion,
|
||||
sessionStart: timestamp,
|
||||
lastUpdate: timestamp,
|
||||
contractId,
|
||||
userId: userId,
|
||||
timerStart: 0,
|
||||
timerEnd: 0,
|
||||
duration: 0,
|
||||
crowdNpcKills: 0,
|
||||
targetKills: new Set(),
|
||||
npcKills: new Set(),
|
||||
bodiesHidden: new Set(),
|
||||
pacifications: new Set(),
|
||||
disguisesUsed: new Set(),
|
||||
disguisesRuined: new Set(),
|
||||
spottedBy: new Set(),
|
||||
witnesses: new Set(),
|
||||
bodiesFoundBy: new Set(),
|
||||
legacyHasBodyBeenFound: false,
|
||||
killsNoticedBy: new Set(),
|
||||
completedObjectives: new Set(),
|
||||
failedObjectives: new Set(),
|
||||
recording: PeacockCameraStatus.NotSpotted,
|
||||
lastAccident: 0,
|
||||
lastKill: {},
|
||||
kills: new Set(),
|
||||
compat: doScoring,
|
||||
markedTargets: new Set(),
|
||||
currentDisguise: "4fc9396e-2619-4e66-a51e-2bd366230da7", // sig suit
|
||||
difficulty,
|
||||
objectiveContexts: new Map(),
|
||||
objectiveStates: new Map(),
|
||||
objectiveDefinitions: new Map(),
|
||||
ghost: {
|
||||
deaths: 0,
|
||||
unnoticedKills: 0,
|
||||
Opponents: [],
|
||||
OpponentScore: 0,
|
||||
Score: 0,
|
||||
IsDraw: false,
|
||||
IsWinner: false,
|
||||
timerEnd: null,
|
||||
},
|
||||
})
|
||||
userIdToTempSession.set(userId, sessionId)
|
||||
|
||||
controller.challengeService.startContract(
|
||||
userId,
|
||||
sessionId,
|
||||
contractSessions.get(sessionId)!,
|
||||
)
|
||||
}
|
||||
|
||||
eventRouter.post(
|
||||
"/SaveAndSynchronizeEvents4",
|
||||
extractToken,
|
||||
jsonMiddleware({ limit: "10Mb" }),
|
||||
(
|
||||
req: RequestWithJwt<
|
||||
unknown,
|
||||
{
|
||||
lastPushDt: number | string
|
||||
lastEventTicks: number | string
|
||||
userId?: string
|
||||
values?: []
|
||||
}
|
||||
>,
|
||||
res,
|
||||
) => {
|
||||
if (req.body.userId !== req.jwt.unique_name) {
|
||||
res.status(403).send() // Trying to save events for other user
|
||||
return
|
||||
}
|
||||
|
||||
if (!Array.isArray(req.body.values)) {
|
||||
res.status(400).end() // malformed request
|
||||
return
|
||||
}
|
||||
|
||||
const savedTokens = req.body.values.length
|
||||
? saveEvents(req.body.userId, req.body.values, req)
|
||||
: null
|
||||
|
||||
let userQueue: S2CEventWithTimestamp[] | undefined
|
||||
let newEvents: ServerToClientEvent[] | null = null
|
||||
|
||||
// events: (server -> client)
|
||||
if ((userQueue = eventQueue.get(req.jwt.unique_name))) {
|
||||
userQueue = userQueue.filter(
|
||||
(item) => item.time > req.body.lastEventTicks,
|
||||
)
|
||||
eventQueue.set(req.jwt.unique_name, userQueue)
|
||||
|
||||
newEvents = Array.from(userQueue, (item) => item.event)
|
||||
}
|
||||
|
||||
// push messages: (server -> client)
|
||||
let userPushQueue: PushMessage[] | undefined
|
||||
let pushMessages: string[] | null = null
|
||||
|
||||
if ((userPushQueue = pushMessageQueue.get(req.jwt.unique_name))) {
|
||||
userPushQueue = userPushQueue.filter(
|
||||
(item) => item.time > req.body.lastPushDt,
|
||||
)
|
||||
pushMessageQueue.set(req.jwt.unique_name, userPushQueue)
|
||||
|
||||
pushMessages = Array.from(userPushQueue, (item) => item.message)
|
||||
}
|
||||
|
||||
res.json({
|
||||
SavedTokens: savedTokens,
|
||||
NewEvents: newEvents || null,
|
||||
NextPoll: 10.0,
|
||||
PushMessages: pushMessages || null,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
eventRouter.post(
|
||||
"/SaveEvents2",
|
||||
extractToken,
|
||||
jsonMiddleware({ limit: "10Mb" }),
|
||||
(req: RequestWithJwt, res) => {
|
||||
if (req.jwt.unique_name !== req.body.userId) {
|
||||
res.status(403).send() // Trying to save events for other user
|
||||
return
|
||||
}
|
||||
|
||||
res.json(saveEvents(req.body.userId, req.body.values, req))
|
||||
},
|
||||
)
|
||||
|
||||
/**
|
||||
* Gets the active session's ID for the specified user (by their ID).
|
||||
*
|
||||
* @param uId The user's ID.
|
||||
* @returns The ID for the user's active session.
|
||||
* @author Reece Dunham
|
||||
*/
|
||||
export function getActiveSessionIdForUser(uId: string): string | undefined {
|
||||
return userIdToTempSession.get(uId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the active session for the specified user (by their ID).
|
||||
*
|
||||
* @param uId The user's ID.
|
||||
* @returns The user's active contract session.
|
||||
* @author Reece Dunham
|
||||
*/
|
||||
export function getSession(uId: string): ContractSession | undefined {
|
||||
const currentSession = getActiveSessionIdForUser(uId)
|
||||
|
||||
if (!currentSession) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return contractSessions.get(currentSession)
|
||||
}
|
||||
|
||||
function contractFailed(
|
||||
event: ClientToServerEvent,
|
||||
session: ContractSession,
|
||||
): void {
|
||||
session.markedTargets.clear()
|
||||
|
||||
const json = controller.resolveContract(session.contractId)!
|
||||
|
||||
let realName: string
|
||||
|
||||
if (json.Metadata.Type === "creation") {
|
||||
realName =
|
||||
event.Value === "Contract ended manually: OnRestartLevel"
|
||||
? "GameRestart"
|
||||
: `ContractFailed:${event.Value}`
|
||||
} else {
|
||||
realName = `ContractFailed:${event.Value}`
|
||||
}
|
||||
|
||||
// if still in cutscene, end mission with 0 time pass -- this will get converted to minimum time within split manager
|
||||
if (session.timerStart !== 0) {
|
||||
// @ts-expect-error TypeScript still hates dates
|
||||
const timeTotal: Seconds = session.timerEnd - session.timerStart
|
||||
liveSplitManager.failMission(timeTotal)
|
||||
} else {
|
||||
liveSplitManager.failMission(0)
|
||||
}
|
||||
|
||||
enqueueEvent(session.userId, {
|
||||
CreatedAt: new Date().toISOString(),
|
||||
Token: process.hrtime.bigint().toString(),
|
||||
Id: randomUUID(),
|
||||
Name: "SegmentClosing",
|
||||
UserId: session.userId,
|
||||
ContractId: session.contractId,
|
||||
SessionId: null,
|
||||
ContractSessionId: event.ContractSessionId,
|
||||
Timestamp: 0.0,
|
||||
Value: {
|
||||
SegmentIndex: 0,
|
||||
LastEventName: "ContractFailed",
|
||||
LastEventTime: (session.lastUpdate as Date).toISOString(),
|
||||
CloseType: realName,
|
||||
},
|
||||
Origin: "ContractSessionService",
|
||||
Version: ServerVer,
|
||||
IsReplicated: false,
|
||||
})
|
||||
}
|
||||
|
||||
function saveEvents(
|
||||
userId: string,
|
||||
events: ClientToServerEvent[],
|
||||
req: RequestWithJwt<unknown, unknown>,
|
||||
): string[] {
|
||||
const response: string[] = []
|
||||
const processed: string[] = []
|
||||
|
||||
events.forEach((event) => {
|
||||
const session = contractSessions.get(event.ContractSessionId)
|
||||
|
||||
if (
|
||||
!session ||
|
||||
session.contractId !== event.ContractId ||
|
||||
session.userId !== userId
|
||||
) {
|
||||
if (PEACOCK_DEV) {
|
||||
log(LogLevel.DEBUG, "No session or session user ID mismatch!")
|
||||
console.debug(session)
|
||||
console.debug(event)
|
||||
}
|
||||
|
||||
return // session does not exist or contractid/userid doesn't match
|
||||
}
|
||||
|
||||
session.duration = event.Timestamp
|
||||
session.lastUpdate = new Date()
|
||||
|
||||
// @ts-expect-error Issue with request type mismatch.
|
||||
controller.hooks.newEvent.call(event, req, session)
|
||||
|
||||
for (const objectiveId of session.objectiveStates.keys()) {
|
||||
try {
|
||||
const objectiveDefinition =
|
||||
session.objectiveDefinitions.get(objectiveId)
|
||||
const objectiveState = session.objectiveStates.get(objectiveId)
|
||||
const objectiveContext =
|
||||
session.objectiveContexts.get(objectiveId)
|
||||
|
||||
const val = handleEvent(
|
||||
objectiveDefinition as never,
|
||||
objectiveContext,
|
||||
event.Value,
|
||||
{
|
||||
eventName: event.Name,
|
||||
currentState: objectiveState,
|
||||
timestamp: event.Timestamp,
|
||||
},
|
||||
)
|
||||
|
||||
if (val.state === "Failure") {
|
||||
if (PEACOCK_DEV) {
|
||||
log(LogLevel.DEBUG, `Objective failed: ${objectiveId}`)
|
||||
}
|
||||
|
||||
session.failedObjectives.add(objectiveId)
|
||||
}
|
||||
|
||||
if (val.context) {
|
||||
session.objectiveContexts.set(objectiveId, val.context)
|
||||
session.objectiveStates.set(objectiveId, val.state)
|
||||
}
|
||||
} catch (e) {
|
||||
log(
|
||||
LogLevel.ERROR,
|
||||
"An error occurred while tracing C2S events, please report this!",
|
||||
)
|
||||
log(LogLevel.ERROR, e)
|
||||
log(LogLevel.ERROR, e.stack)
|
||||
}
|
||||
}
|
||||
|
||||
controller.challengeService.onContractEvent(
|
||||
event,
|
||||
event.ContractSessionId,
|
||||
session,
|
||||
)
|
||||
|
||||
// these events are important but may be fired after the timer is over
|
||||
const canGetAfterTimerOver = [
|
||||
"ContractEnd",
|
||||
"ObjectiveCompleted",
|
||||
"CpdSet",
|
||||
]
|
||||
|
||||
if (
|
||||
!canGetAfterTimerOver.includes(event.Name) &&
|
||||
session.timerEnd !== 0 &&
|
||||
event.Timestamp > session.timerEnd
|
||||
) {
|
||||
// Do not handle events that occur after exiting the level
|
||||
response.push(process.hrtime.bigint().toString())
|
||||
return
|
||||
}
|
||||
|
||||
// @ts-expect-error Tapable types not sufficient
|
||||
controller.hooks.newEvent.call(event, req)
|
||||
|
||||
const contract = controller.resolveContract(session.contractId)
|
||||
const contractType = contract?.Metadata?.Type?.toLowerCase()
|
||||
|
||||
if (handleMultiplayerEvent(event, session)) {
|
||||
processed.push(event.Name)
|
||||
}
|
||||
|
||||
switch (event.Name) {
|
||||
case "HeroSpawn_Location":
|
||||
liveSplitManager.missionIntentResolved(
|
||||
event.ContractId,
|
||||
(<HeroSpawn_LocationC2SEvent>event).Value.RepositoryId,
|
||||
)
|
||||
break
|
||||
case "Kill": {
|
||||
const killValue = (event as KillC2SEvent).Value
|
||||
|
||||
if (session.lastKill.timestamp === event.Timestamp) {
|
||||
session.lastKill.repositoryIds?.push(killValue.RepositoryId)
|
||||
} else {
|
||||
session.lastKill = {
|
||||
timestamp: event.Timestamp,
|
||||
repositoryIds: [killValue.RepositoryId],
|
||||
}
|
||||
}
|
||||
|
||||
if (killValue.KillContext === EDeathContext.eDC_NOT_HERO) {
|
||||
// this is not 47, so we keep silent assassin
|
||||
log(
|
||||
LogLevel.DEBUG,
|
||||
`${killValue.RepositoryId} eliminated, 47 not responsible`,
|
||||
)
|
||||
response.push(process.hrtime.bigint().toString())
|
||||
return
|
||||
}
|
||||
|
||||
log(
|
||||
LogLevel.DEBUG,
|
||||
`Actor ${killValue.RepositoryId} eliminated.`,
|
||||
)
|
||||
|
||||
if (killValue.IsTarget || contractType === "creation") {
|
||||
const kill: RatingKill = {
|
||||
KillClass: killValue.KillClass,
|
||||
KillMethodBroad: killValue.KillMethodBroad,
|
||||
KillItemCategory: killValue.KillItemCategory,
|
||||
IsHeadshot: killValue.IsHeadshot,
|
||||
KillMethodStrict: killValue.KillMethodStrict,
|
||||
KillItemRepositoryId: killValue.KillItemRepositoryId,
|
||||
_RepositoryId: killValue.RepositoryId,
|
||||
OutfitRepoId: session.currentDisguise,
|
||||
}
|
||||
|
||||
session.kills.add(kill)
|
||||
|
||||
session.targetKills.add(killValue.RepositoryId)
|
||||
} else {
|
||||
session.npcKills.add(killValue.RepositoryId)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "CrowdNPC_Died":
|
||||
session.crowdNpcKills += 1
|
||||
break
|
||||
case "Pacify":
|
||||
session.pacifications.add(
|
||||
(<PacifyC2SEvent>event).Value.RepositoryId,
|
||||
)
|
||||
break
|
||||
case "BodyHidden":
|
||||
session.bodiesHidden.add(
|
||||
(<BodyHiddenC2SEvent>event).Value.RepositoryId,
|
||||
)
|
||||
break
|
||||
case "BodyFound":
|
||||
if (req.gameVersion === "h1") {
|
||||
session.legacyHasBodyBeenFound = true
|
||||
}
|
||||
break
|
||||
case "Disguise":
|
||||
log(LogLevel.DEBUG, `Now disguised: ${event.Value as string}`)
|
||||
session.currentDisguise = event.Value as string
|
||||
session.disguisesUsed.add(event.Value as string)
|
||||
break
|
||||
case "ContractStart": {
|
||||
const disguise = (<ContractStartC2SEvent>event).Value.Disguise
|
||||
|
||||
session.currentDisguise = disguise
|
||||
session.disguisesUsed.add(disguise)
|
||||
liveSplitManager.startMission(
|
||||
session.contractId,
|
||||
req.gameVersion,
|
||||
req.jwt.unique_name,
|
||||
)
|
||||
break
|
||||
}
|
||||
case "DisguiseBlown":
|
||||
session.disguisesRuined.add(event.Value as string)
|
||||
break
|
||||
case "BrokenDisguiseCleared":
|
||||
session.disguisesRuined.delete(event.Value as string)
|
||||
break
|
||||
case "Spotted":
|
||||
for (const actor of (event as SpottedC2SEvent).Value) {
|
||||
session.spottedBy.add(actor)
|
||||
}
|
||||
break
|
||||
case "Witnesses":
|
||||
for (const actor of (event as WitnessesC2SEvent).Value) {
|
||||
session.witnesses.add(actor)
|
||||
}
|
||||
break
|
||||
case "SecuritySystemRecorder": {
|
||||
const eventValue = (<SecuritySystemRecorderC2SEvent>event).Value
|
||||
if (
|
||||
eventValue.event === "spotted" &&
|
||||
session.recording !== PeacockCameraStatus.Erased
|
||||
) {
|
||||
session.recording = PeacockCameraStatus.Spotted
|
||||
} else if (
|
||||
eventValue.event === "destroyed" ||
|
||||
eventValue.event === "erased"
|
||||
) {
|
||||
session.recording = PeacockCameraStatus.Erased
|
||||
}
|
||||
break
|
||||
}
|
||||
case "IntroCutEnd":
|
||||
if (!session.timerStart) {
|
||||
session.timerStart = event.Timestamp
|
||||
}
|
||||
break
|
||||
case "exit_gate":
|
||||
session.timerEnd = event.Timestamp
|
||||
break
|
||||
case "ContractEnd":
|
||||
if (!session.timerEnd) {
|
||||
session.timerEnd = event.Timestamp
|
||||
}
|
||||
break
|
||||
case "ObjectiveCompleted":
|
||||
session.completedObjectives.add(
|
||||
(<ObjectiveCompletedC2SEvent>event).Value.Id,
|
||||
)
|
||||
break
|
||||
case "AccidentBodyFound":
|
||||
session.lastAccident = event.Timestamp
|
||||
break
|
||||
case "MurderedBodySeen":
|
||||
if (
|
||||
(event.Timestamp as unknown as number) !==
|
||||
session.lastAccident
|
||||
) {
|
||||
session.bodiesFoundBy.add(
|
||||
(<MurderedBodySeenC2SEvent>event).Value.Witness,
|
||||
)
|
||||
if (event.Timestamp === session.lastKill.timestamp) {
|
||||
session.killsNoticedBy.add(
|
||||
(<MurderedBodySeenC2SEvent>event).Value.Witness,
|
||||
)
|
||||
}
|
||||
}
|
||||
break
|
||||
case "ActorTagged": {
|
||||
const val = (<ActorTaggedC2SEvent>event).Value
|
||||
|
||||
if (!val.Tagged) {
|
||||
session.markedTargets.delete(val.RepositoryId)
|
||||
} else if (val.Tagged) {
|
||||
session.markedTargets.add(val.RepositoryId)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "ItemPickedUp":
|
||||
log(
|
||||
LogLevel.INFO,
|
||||
`Picked up item with repository ID: ${
|
||||
(<ItemPickedUpC2SEvent>event).Value.RepositoryId
|
||||
}`,
|
||||
)
|
||||
break
|
||||
case "StartingSuit":
|
||||
session.currentDisguise = event.Value as string
|
||||
break
|
||||
case "ContractFailed":
|
||||
session.timerEnd = event.Timestamp
|
||||
contractFailed(event, session)
|
||||
break
|
||||
case "setpieces":
|
||||
log(
|
||||
LogLevel.DEBUG,
|
||||
`Setpiece: ${
|
||||
(<SetpiecesC2SEvent>event).Value.RepositoryId
|
||||
}`,
|
||||
)
|
||||
break
|
||||
case "ItemDropped":
|
||||
log(
|
||||
LogLevel.DEBUG,
|
||||
`Item dropped: ${
|
||||
(<ItemDroppedC2SEvent>event).Value.RepositoryId
|
||||
}`,
|
||||
)
|
||||
break
|
||||
case "AmbientChanged":
|
||||
log(
|
||||
LogLevel.DEBUG,
|
||||
`Ambient switched to ${
|
||||
(<AmbientChangedC2SEvent>event).Value.AmbientValue
|
||||
}`,
|
||||
)
|
||||
break
|
||||
case "Hero_Health":
|
||||
case "NPC_Distracted":
|
||||
case "ShotsHit":
|
||||
case "FirstNonHeadshot":
|
||||
case "OpportunityEvents":
|
||||
case "FirstMissedShot":
|
||||
// we don't care about these
|
||||
break
|
||||
default:
|
||||
// no-op on our part
|
||||
break
|
||||
}
|
||||
|
||||
processed.push(event.Name)
|
||||
|
||||
response.push(process.hrtime.bigint().toString())
|
||||
})
|
||||
|
||||
if (PEACOCK_DEV && processed.length > 0) {
|
||||
log(
|
||||
LogLevel.DEBUG,
|
||||
`Event summary: ${picocolors.gray(processed.join(", "))}`,
|
||||
)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
export async function saveSession(
|
||||
sessionId: string,
|
||||
token: string,
|
||||
): Promise<void> {
|
||||
if (!contractSessions.has(sessionId)) {
|
||||
log(LogLevel.WARN, `Refusing to save ${sessionId} as it doesn't exist`)
|
||||
return
|
||||
}
|
||||
|
||||
await writeContractSession(
|
||||
token + "_" + sessionId,
|
||||
contractSessions.get(sessionId)!,
|
||||
)
|
||||
}
|
||||
|
||||
export async function loadSession(
|
||||
sessionId: string,
|
||||
token: string,
|
||||
sessionData?: ContractSession,
|
||||
): Promise<void> {
|
||||
if (!sessionData) {
|
||||
sessionData = await getContractSession(token + "_" + sessionId)
|
||||
}
|
||||
|
||||
contractSessions.set(sessionId, sessionData)
|
||||
}
|
||||
|
||||
export { eventRouter }
|
170
components/flags.ts
Normal file
170
components/flags.ts
Normal file
@ -0,0 +1,170 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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 { existsSync, readFileSync, unlinkSync, writeFileSync } from "fs"
|
||||
import type { Flags } from "./types/types"
|
||||
import { log, LogLevel } from "./loggingInterop"
|
||||
import { parse } from "js-ini"
|
||||
import type { IIniObject } from "js-ini/lib/interfaces/ini-object"
|
||||
|
||||
let flags: IIniObject = {}
|
||||
|
||||
const defaultFlags: Flags = {
|
||||
discordRp: {
|
||||
desc: "Toggle Discord rich presence on or off.",
|
||||
default: false,
|
||||
},
|
||||
discordRpAppTime: {
|
||||
desc: "For Discord Rich Presence, if set to false, the time playing the current level will be shown, and if set to true, the total time using Peacock will be shown.",
|
||||
default: false,
|
||||
},
|
||||
officialAuthentication: {
|
||||
desc: "Use official servers for contract downloading",
|
||||
default: true,
|
||||
},
|
||||
autoSplitterCampaign: {
|
||||
desc: "Which (main) campaign to use for the AutoSplitter. Can be set to 1, 2, 3, or 'trilogy'.",
|
||||
default: "trilogy",
|
||||
},
|
||||
autoSplitterRacetimegg: {
|
||||
desc: "When set to true, autosplitter is set in a special mode for use with livesplit integration for racetime.gg realtime races.",
|
||||
default: false,
|
||||
},
|
||||
autoSplitterForceSilentAssassin: {
|
||||
desc: "When set to true, the autosplitter will only accept missions completed with silent assassin to be valid completions. When false, any completion will split.",
|
||||
default: true,
|
||||
},
|
||||
jokes: {
|
||||
desc: "The Peacock server window will tell you a joke on startup if this is set to true.",
|
||||
default: false,
|
||||
},
|
||||
leaderboardsHost: {
|
||||
desc: "Please do not modify - intended for development only",
|
||||
default: "https://backend.rdil.rocks",
|
||||
},
|
||||
leaderboards: {
|
||||
desc: "Allow your times to be submitted to the ingame leaderboards. If you do not want your times on the leaderboards, change this to false.",
|
||||
default: true,
|
||||
},
|
||||
updateChecking: {
|
||||
desc: "Allow Peacock to check for updates on startup.",
|
||||
default: true,
|
||||
},
|
||||
loadoutSaving: {
|
||||
desc: "Default loadout mode - either PROFILES (loadout profiles) or LEGACY for per-user saving",
|
||||
default: "PROFILES",
|
||||
},
|
||||
elusivesAreShown: {
|
||||
desc: "Show elusive targets in instinct like normal targets would appear on normal missions. (for speedrunners who are submitting to speedrun.com, just as a reminder, this tool is for practice only!)",
|
||||
default: false,
|
||||
},
|
||||
imageLoading: {
|
||||
desc: "How images are loaded. SAVEASREQUESTED will fetch images from online when needed (and save them in the images folder), ONLINE will fetch them without saving, and OFFLINE will load them from the image folder",
|
||||
default: "SAVEASREQUESTED",
|
||||
},
|
||||
overrideFrameworkChecks: {
|
||||
desc: "Forcibly disable installed mod checks",
|
||||
default: false,
|
||||
},
|
||||
}
|
||||
|
||||
const OLD_FLAGS_FILE = "flags.json5"
|
||||
const NEW_FLAGS_FILE = "options.ini"
|
||||
|
||||
/**
|
||||
* Get a flag from the flag file.
|
||||
*
|
||||
* @param flagId The flag's name.
|
||||
* @returns The flag's value.
|
||||
*/
|
||||
export function getFlag(flagId: string): string | boolean | number {
|
||||
return (
|
||||
(flags[flagId] as string | boolean | number) ??
|
||||
defaultFlags[flagId].default
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* At this point, you may be asking "what on Earth does this do?" - I completely understand.
|
||||
*
|
||||
* It should do something along the lines of generating a string that is the flags
|
||||
* file with the appropriate comments (js-ini's stringify doesn't support them),
|
||||
* and all the flags will either be the default value, or what they are set to already.
|
||||
*/
|
||||
const makeFlagsIni = (
|
||||
_flags: IIniObject | { desc: string; default: string }[],
|
||||
): string =>
|
||||
Object.keys(defaultFlags)
|
||||
.map((flagId) => {
|
||||
return `; ${defaultFlags[flagId].desc}
|
||||
${flagId} = ${_flags[flagId]}`
|
||||
})
|
||||
.join("\n\n")
|
||||
|
||||
/**
|
||||
* Loads all flags.
|
||||
*/
|
||||
export function loadFlags(): void {
|
||||
// somebody please, clean this method up, I hate it
|
||||
if (existsSync(OLD_FLAGS_FILE)) {
|
||||
log(
|
||||
LogLevel.WARN,
|
||||
"The flags file (flags.json5) has been revamped in the latest Peacock version, and we had to remove your settings.",
|
||||
)
|
||||
log(
|
||||
LogLevel.INFO,
|
||||
"You can take a look at the new options.ini file, which includes descriptions and more!",
|
||||
)
|
||||
|
||||
unlinkSync(OLD_FLAGS_FILE)
|
||||
}
|
||||
|
||||
if (!existsSync(NEW_FLAGS_FILE)) {
|
||||
const allTheFlags = {}
|
||||
|
||||
Object.keys(defaultFlags).forEach((f) => {
|
||||
allTheFlags[f] = defaultFlags[f].default
|
||||
})
|
||||
|
||||
const ini = makeFlagsIni(allTheFlags)
|
||||
|
||||
writeFileSync(NEW_FLAGS_FILE, ini)
|
||||
}
|
||||
|
||||
flags = parse(readFileSync(NEW_FLAGS_FILE).toString())
|
||||
|
||||
Object.keys(defaultFlags).forEach((key) => {
|
||||
if (!Object.prototype.hasOwnProperty.call(flags, key)) {
|
||||
flags[key] = defaultFlags[key].default
|
||||
}
|
||||
})
|
||||
|
||||
writeFileSync(NEW_FLAGS_FILE, makeFlagsIni(flags))
|
||||
|
||||
log(LogLevel.DEBUG, "Loaded flags.")
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the values of all flags. Only intended for debugging purposes, since this could cause memory issues.
|
||||
*
|
||||
* @internal
|
||||
* @return The flags.
|
||||
*/
|
||||
export function getAllFlags(): IIniObject {
|
||||
return flags
|
||||
}
|
258
components/hooksImpl.ts
Normal file
258
components/hooksImpl.ts
Normal file
@ -0,0 +1,258 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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<Hook> {
|
||||
private readonly _map: Map<string, Hook>
|
||||
|
||||
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> = T extends any[] ? T : [T]
|
||||
|
||||
/**
|
||||
* An internal interface containing the properties held by a single taps' container object.
|
||||
*/
|
||||
interface Tap<T, R> {
|
||||
name: string
|
||||
func: (...args: AsArray<T>) => R
|
||||
enableContext: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* The structure of an intercept.
|
||||
*
|
||||
* @see name
|
||||
* @see call
|
||||
* @see tap
|
||||
*/
|
||||
export interface Intercept<Params, Return> {
|
||||
/**
|
||||
* 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<Params>): 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<Params>) => Return): void
|
||||
}
|
||||
|
||||
/**
|
||||
* The base for a hook, including {@link tap} and {@link intercept} functionality.
|
||||
*
|
||||
* @see SyncHook
|
||||
* @see SyncBailHook
|
||||
* @see AsyncSeriesHook
|
||||
*/
|
||||
export abstract class BaseImpl<Params, Return = void> {
|
||||
protected _intercepts: Intercept<Params, Return>[]
|
||||
protected _taps: Tap<Params, Return>[]
|
||||
|
||||
/**
|
||||
* 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<Params, Return>): 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<Params>) => 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A hook that runs each tap one-by-one.
|
||||
*/
|
||||
export class SyncHook<Params> extends BaseImpl<Params> {
|
||||
public constructor() {
|
||||
super()
|
||||
this._taps = []
|
||||
this._intercepts = []
|
||||
}
|
||||
|
||||
public call(...params: AsArray<Params>): 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<Params, Return> extends BaseImpl<Params, Return> {
|
||||
public constructor() {
|
||||
super()
|
||||
this._taps = []
|
||||
this._intercepts = []
|
||||
}
|
||||
|
||||
public call(...params: AsArray<Params>): 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<Params> extends BaseImpl<Params, Promise<void>> {
|
||||
public constructor() {
|
||||
super()
|
||||
this._taps = []
|
||||
this._intercepts = []
|
||||
}
|
||||
|
||||
/**
|
||||
* Async hooks cannot be called from a sync function - use {@link callAsync} instead!
|
||||
* This function will only throw an error.
|
||||
*
|
||||
* @throws {Error} Always throws an error, see the note above.
|
||||
* @deprecated
|
||||
*/
|
||||
public call(): Promise<void> {
|
||||
throw new Error("Can't call an async hook with the sync method.")
|
||||
}
|
||||
|
||||
public async callAsync(...params: AsArray<Params>): Promise<void> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
44
components/hotReloadService.ts
Normal file
44
components/hotReloadService.ts
Normal file
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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 { FileChangeInfo, watch } from "fs/promises"
|
||||
|
||||
/**
|
||||
* Set up a listener for file/folder changes.
|
||||
*
|
||||
* @param target The file or folder name to watch.
|
||||
* @param callback What to do when the hot updater is triggered.
|
||||
*/
|
||||
export async function setupHotListener(
|
||||
target: string,
|
||||
callback: (event: FileChangeInfo<string>) => void,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const watcher = watch(target, {})
|
||||
|
||||
for await (const event of watcher) {
|
||||
callback(event)
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.name === "AbortError") {
|
||||
return
|
||||
}
|
||||
|
||||
throw err
|
||||
}
|
||||
}
|
630
components/index.ts
Normal file
630
components/index.ts
Normal file
@ -0,0 +1,630 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-inner-declarations */
|
||||
// noinspection RequiredAttributes
|
||||
|
||||
import { setFlagsFromString } from "v8"
|
||||
import { program } from "commander"
|
||||
import express, { Request, Router } from "express"
|
||||
import http from "http"
|
||||
import {
|
||||
checkForUpdates,
|
||||
extractToken,
|
||||
handleAxiosError,
|
||||
IS_LAUNCHER,
|
||||
jokes,
|
||||
PEACOCKVER,
|
||||
PEACOCKVERSTRING,
|
||||
ServerVer,
|
||||
} from "./utils"
|
||||
import { getConfig, getSwizzleable, swizzle } from "./configSwizzleManager"
|
||||
import { handleOauthToken } from "./oauthToken"
|
||||
import type {
|
||||
RequestWithJwt,
|
||||
S2CEventWithTimestamp,
|
||||
ServerConnectionConfig,
|
||||
} from "./types/types"
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"
|
||||
import { join } from "path"
|
||||
import { log, loggingMiddleware, LogLevel } from "./loggingInterop"
|
||||
import { eventRouter } from "./eventHandler"
|
||||
import { contractRoutingRouter } from "./contracts/contractRouting"
|
||||
import { profileRouter } from "./profileHandler"
|
||||
import { firstPassRouter, menuDataRouter } from "./menuData"
|
||||
import { menuSystemRouter } from "./menus/menuSystem"
|
||||
import { legacyEventRouter } from "./2016/legacyEventRouter"
|
||||
import { legacyMenuSystemRouter } from "./2016/legacyMenuSystem"
|
||||
import { _theLastYardbirdScpc, controller } from "./controller"
|
||||
import {
|
||||
STEAM_NAMESPACE_2016,
|
||||
STEAM_NAMESPACE_2018,
|
||||
STEAM_NAMESPACE_2021,
|
||||
STEAM_NAMESPACE_SCPC,
|
||||
} from "./platformEntitlements"
|
||||
import { legacyProfileRouter } from "./2016/legacyProfileRouter"
|
||||
import { legacyMenuDataRouter } from "./2016/legacyMenuData"
|
||||
import { legacyContractRouter } from "./2016/legacyContractHandler"
|
||||
import { getFlag, loadFlags } from "./flags"
|
||||
import { initRp } from "./discordRp"
|
||||
import random from "random"
|
||||
import { generateUserCentric } from "./contracts/dataGen"
|
||||
import { json as jsonMiddleware, urlencoded } from "body-parser"
|
||||
import { loadoutRouter, loadouts } from "./loadouts"
|
||||
import { setupHotListener } from "./hotReloadService"
|
||||
import type { AxiosError } from "axios"
|
||||
import serveStatic from "serve-static"
|
||||
import { webFeaturesRouter } from "./webFeatures"
|
||||
import { toolsMenu } from "./tools"
|
||||
import picocolors from "picocolors"
|
||||
import { multiplayerRouter } from "./multiplayer/multiplayerService"
|
||||
import { multiplayerMenuDataRouter } from "./multiplayer/multiplayerMenuData"
|
||||
import { pack, unpack } from "msgpackr"
|
||||
|
||||
// welcome to the bleeding edge
|
||||
setFlagsFromString("--harmony")
|
||||
|
||||
const host = process.env.HOST || "0.0.0.0"
|
||||
const port = process.env.PORT || 80
|
||||
|
||||
function uncaught(error: Error): void {
|
||||
if (
|
||||
(error.message || "").includes("EADDRINUSE") ||
|
||||
(error.stack || "").includes("EADDRINUSE")
|
||||
) {
|
||||
log(LogLevel.ERROR, `Failed to use the server on ${host}:${port}!`)
|
||||
log(
|
||||
LogLevel.ERROR,
|
||||
"This is likely due to one of the following reasons:",
|
||||
)
|
||||
log(LogLevel.ERROR, ` - Peacock is already running on this port`)
|
||||
log(
|
||||
LogLevel.ERROR,
|
||||
` - Another app is already using this port (like IIS server)`,
|
||||
)
|
||||
log(
|
||||
LogLevel.ERROR,
|
||||
` - Your user account doesn't have permission (firewall can block it)`,
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if ((error as AxiosError).isAxiosError) {
|
||||
handleAxiosError(error as AxiosError)
|
||||
}
|
||||
|
||||
log(LogLevel.ERROR, error.message)
|
||||
error.stack && log(LogLevel.ERROR, error.stack)
|
||||
}
|
||||
|
||||
process.on("uncaughtException", uncaught)
|
||||
|
||||
loadFlags()
|
||||
|
||||
const app = express()
|
||||
|
||||
app.use(loggingMiddleware)
|
||||
app.use("/_wf", webFeaturesRouter)
|
||||
|
||||
if (getFlag("loadoutSaving") === "PROFILES") {
|
||||
app.use("/loadouts", loadoutRouter)
|
||||
}
|
||||
|
||||
app.get("/", (req: Request, res) => {
|
||||
if (PEACOCK_DEV) {
|
||||
res.send("dev active, you need to access the UI from port 3000")
|
||||
} else {
|
||||
const data = readFileSync("webui/dist/index.html").toString()
|
||||
|
||||
res.send(data)
|
||||
}
|
||||
})
|
||||
|
||||
app.use("/assets", serveStatic("webui/dist/assets"))
|
||||
|
||||
app.get(
|
||||
"/config/:audience/:serverVersion(\\d+_\\d+_\\d+)",
|
||||
(req: RequestWithJwt<{ issuer: string }>, res) => {
|
||||
const proto = req.protocol
|
||||
const config = getConfig("config", true) as ServerConnectionConfig
|
||||
const serverhost = req.get("Host")
|
||||
|
||||
config.Versions[0].GAME_VER = req.params.serverVersion.startsWith("8")
|
||||
? `${ServerVer._Major}.${ServerVer._Minor}.${ServerVer._Build}`
|
||||
: req.params.serverVersion.startsWith("7")
|
||||
? "7.17.0"
|
||||
: "6.74.0"
|
||||
|
||||
if (req.params.serverVersion.startsWith("8")) {
|
||||
config.Versions[0].SERVER_VER.GlobalAuthentication.RequestedAudience =
|
||||
"pc-prod_8"
|
||||
}
|
||||
|
||||
if (req.params.serverVersion.startsWith("7")) {
|
||||
config.Versions[0].SERVER_VER.GlobalAuthentication.RequestedAudience =
|
||||
"pc-prod_7"
|
||||
}
|
||||
|
||||
if (req.params.serverVersion.startsWith("6")) {
|
||||
config.Versions[0].SERVER_VER.GlobalAuthentication.RequestedAudience =
|
||||
"pc-prod_6"
|
||||
}
|
||||
|
||||
if (req.query.issuer === STEAM_NAMESPACE_2021) {
|
||||
config.Versions[0].SERVER_VER.GlobalAuthentication.RequestedAudience =
|
||||
"steam-prod_8"
|
||||
}
|
||||
|
||||
if (req.params.audience === "scpc-prod") {
|
||||
log(LogLevel.DEBUG, "Entering special mode.")
|
||||
// sniper challenge is a different game/audience
|
||||
config.Versions[0].Name = "scpc-prod"
|
||||
config.Versions[0].GAME_VER = "7.3.0"
|
||||
config.Versions[0].SERVER_VER.GlobalAuthentication.RequestedAudience =
|
||||
"scpc-prod"
|
||||
}
|
||||
|
||||
config.Versions[0].ISSUER_ID = req.query.issuer || "*"
|
||||
|
||||
config.Versions[0].SERVER_VER.Metrics.MetricsServerHost = `${proto}://${serverhost}`
|
||||
|
||||
config.Versions[0].SERVER_VER.Authentication.AuthenticationHost = `${proto}://${serverhost}`
|
||||
|
||||
config.Versions[0].SERVER_VER.Configuration.Url = `${proto}://${serverhost}/files/onlineconfig.json`
|
||||
|
||||
config.Versions[0].SERVER_VER.Configuration.AgreementUrl = `${proto}://${serverhost}/files/privacypolicy/hm3/privacypolicy.json`
|
||||
|
||||
config.Versions[0].SERVER_VER.Resources.ResourcesServicePath = `${proto}://${serverhost}/files`
|
||||
|
||||
config.Versions[0].SERVER_VER.GlobalAuthentication.AuthenticationHost = `${proto}://${serverhost}`
|
||||
|
||||
res.json(config)
|
||||
},
|
||||
)
|
||||
|
||||
app.get("/files/privacypolicy/hm3/privacypolicy_*.json", (req, res) => {
|
||||
res.set("Content-Type", "application/octet-stream")
|
||||
res.set("x-ms-meta-version", "20181001")
|
||||
res.send(getConfig("privacypolicy", false))
|
||||
})
|
||||
|
||||
app.post(
|
||||
"/api/metrics/*",
|
||||
jsonMiddleware(),
|
||||
(req: RequestWithJwt<never, S2CEventWithTimestamp[]>, res) => {
|
||||
req.body.forEach((event) => {
|
||||
controller.hooks.newMetricsEvent.call(event, req)
|
||||
})
|
||||
|
||||
res.send()
|
||||
},
|
||||
)
|
||||
|
||||
app.use("/oauth/token", urlencoded())
|
||||
|
||||
app.post("/oauth/token", (req: RequestWithJwt, res) =>
|
||||
handleOauthToken(req, res),
|
||||
)
|
||||
|
||||
app.get("/files/onlineconfig.json", (req, res) => {
|
||||
res.set("Content-Type", "application/octet-stream")
|
||||
res.send(getConfig("onlineconfig", false))
|
||||
})
|
||||
|
||||
app.get(
|
||||
"/profiles/page//dashboard//Dashboard_Category_Sniper_Singleplayer/00000000-0000-0000-0000-000000000015/Contract/ff9f46cf-00bd-4c12-b887-eac491c3a96d",
|
||||
(req: RequestWithJwt, res) => {
|
||||
res.json({
|
||||
template: getConfig("FrankensteinMmSpTemplate", false),
|
||||
data: {
|
||||
Item: {
|
||||
Id: "ff9f46cf-00bd-4c12-b887-eac491c3a96d",
|
||||
Type: "Contract",
|
||||
Title: "UI_CONTRACT_HAWK_TITLE",
|
||||
Date: new Date().toISOString(),
|
||||
Data: generateUserCentric(
|
||||
_theLastYardbirdScpc,
|
||||
req.jwt.unique_name,
|
||||
"h1",
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
// We handle this for now, but it's not used. For the future though.
|
||||
app.get(
|
||||
"/profiles/page//dashboard//Dashboard_Category_Sniper_Multiplayer/00000000-0000-0000-0000-000000000015/Contract/ff9f46cf-00bd-4c12-b887-eac491c3a96d",
|
||||
(req: RequestWithJwt, res) => {
|
||||
const template = getConfig("FrankensteinMmMpTemplate", false)
|
||||
|
||||
/* To enable multiplayer:
|
||||
* Change MultiplayerNotSupported to false
|
||||
* NOTE: REMOVING THIS FULLY WILL BREAK THE EDITED TEMPLATE!
|
||||
*/
|
||||
|
||||
res.json({
|
||||
template: template,
|
||||
data: {
|
||||
Item: {
|
||||
Id: "ff9f46cf-00bd-4c12-b887-eac491c3a96d",
|
||||
Type: "Contract",
|
||||
Title: "UI_CONTRACT_HAWK_TITLE",
|
||||
Date: new Date().toISOString(),
|
||||
Disabled: true,
|
||||
Data: {
|
||||
...generateUserCentric(
|
||||
_theLastYardbirdScpc,
|
||||
req.jwt.unique_name,
|
||||
"h1",
|
||||
),
|
||||
...{ MultiplayerNotSupported: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
// NOTE! All routes attached after this point will be checked for a JWT or blob signature.
|
||||
// If you are adding a route that does NOT require authentication, put it ABOVE this message!
|
||||
|
||||
app.use(
|
||||
Router()
|
||||
.use(
|
||||
"/resources-:serverVersion(\\d+-\\d+)/",
|
||||
(req: RequestWithJwt, res, next) => {
|
||||
req.serverVersion = req.params.serverVersion
|
||||
req.gameVersion = req.serverVersion.startsWith("8")
|
||||
? "h3"
|
||||
: req.serverVersion.startsWith("7") &&
|
||||
// prettier-ignore
|
||||
req.serverVersion !== "7.3.0"
|
||||
? // prettier-ignore
|
||||
"h2"
|
||||
: // prettier-ignore
|
||||
"h1"
|
||||
|
||||
if (req.serverVersion === "7.3.0") {
|
||||
req.gameVersion = "scpc"
|
||||
}
|
||||
|
||||
next("router")
|
||||
},
|
||||
)
|
||||
// we're fine with skipping to the next router if we don't have auth
|
||||
.use(extractToken, (req: RequestWithJwt, res, next) => {
|
||||
switch (req.jwt?.pis) {
|
||||
case "egp_io_interactive_hitman_the_complete_first_season":
|
||||
case STEAM_NAMESPACE_2016:
|
||||
case STEAM_NAMESPACE_SCPC:
|
||||
req.serverVersion = "6-74"
|
||||
break
|
||||
case STEAM_NAMESPACE_2018:
|
||||
req.serverVersion = "7-17"
|
||||
break
|
||||
case "fghi4567xQOCheZIin0pazB47qGUvZw4":
|
||||
case STEAM_NAMESPACE_2021:
|
||||
req.serverVersion = "8-10"
|
||||
break
|
||||
default:
|
||||
res.status(400).json({ message: "no game data" })
|
||||
return
|
||||
}
|
||||
|
||||
req.gameVersion = req.serverVersion.startsWith("8")
|
||||
? "h3"
|
||||
: req.serverVersion.startsWith("7")
|
||||
? "h2"
|
||||
: "h1"
|
||||
|
||||
if (req.jwt?.aud === "scpc-prod") {
|
||||
req.gameVersion = "scpc"
|
||||
}
|
||||
|
||||
next()
|
||||
}),
|
||||
)
|
||||
|
||||
function generateBlobConfig(req: RequestWithJwt) {
|
||||
return {
|
||||
bloburl: `${req.protocol}://${req.get("Host")}/resources-${
|
||||
req.serverVersion
|
||||
}/`,
|
||||
blobsig: `?sv=2018-03-28&ver=${req.gameVersion}`,
|
||||
blobsigduration: 7200000.0,
|
||||
}
|
||||
}
|
||||
|
||||
app.get(
|
||||
"/authentication/api/configuration/Init?*",
|
||||
extractToken,
|
||||
(req: RequestWithJwt, res) => {
|
||||
// configName=pc-prod&lockedContentDisabled=false&isFreePrologueUser=false&isIntroPackUser=false&isFullExperienceUser=false
|
||||
res.json({
|
||||
token: `${req.jwt.exp}-${req.jwt.nbf}-${req.jwt.platform}-${req.jwt.userid}`,
|
||||
blobconfig: generateBlobConfig(req),
|
||||
profileid: req.jwt.unique_name,
|
||||
serverversion: `${ServerVer._Major}.${ServerVer._Minor}.${ServerVer._Build}.${ServerVer._Revision}`,
|
||||
servertimeutc: new Date().toISOString(),
|
||||
ias: 2,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
app.post(
|
||||
"/authentication/api/userchannel/AuthenticationService/RenewBlobSignature",
|
||||
(req: RequestWithJwt, res) => {
|
||||
res.json(generateBlobConfig(req))
|
||||
},
|
||||
)
|
||||
|
||||
const legacyRouter = Router()
|
||||
const primaryRouter = Router()
|
||||
|
||||
legacyRouter.use(
|
||||
"/authentication/api/userchannel/EventsService/",
|
||||
legacyEventRouter,
|
||||
)
|
||||
legacyRouter.use("/resources-(\\d+-\\d+)/", legacyMenuSystemRouter)
|
||||
legacyRouter.use("/authentication/api/userchannel/", legacyProfileRouter)
|
||||
legacyRouter.use("/profiles/page/", legacyMenuDataRouter)
|
||||
legacyRouter.use(
|
||||
"/authentication/api/userchannel/ContractsService/",
|
||||
legacyContractRouter,
|
||||
)
|
||||
legacyRouter.use(
|
||||
"/authentication/api/userchannel/ContractSessionsService/",
|
||||
legacyContractRouter,
|
||||
)
|
||||
|
||||
primaryRouter.use(
|
||||
"/authentication/api/userchannel/MultiplayerService/",
|
||||
multiplayerRouter,
|
||||
)
|
||||
primaryRouter.use("/authentication/api/userchannel/EventsService/", eventRouter)
|
||||
primaryRouter.use(
|
||||
"/authentication/api/userchannel/ContractsService/",
|
||||
contractRoutingRouter,
|
||||
)
|
||||
primaryRouter.use("/authentication/api/userchannel/", profileRouter)
|
||||
primaryRouter.use("/profiles/page/", firstPassRouter)
|
||||
primaryRouter.use("/profiles/page", multiplayerMenuDataRouter)
|
||||
primaryRouter.use("/profiles/page/", menuDataRouter)
|
||||
primaryRouter.use("/resources-(\\d+-\\d+)/", menuSystemRouter)
|
||||
|
||||
app.use(
|
||||
Router()
|
||||
.use((req: RequestWithJwt, res, next) => {
|
||||
if (req.shouldCease) {
|
||||
return next("router")
|
||||
}
|
||||
|
||||
if (req.serverVersion === "6-74" || req.serverVersion === "7-3") {
|
||||
return next() // continue along h1router
|
||||
}
|
||||
|
||||
next("router")
|
||||
})
|
||||
.use(legacyRouter),
|
||||
Router()
|
||||
.use((req: RequestWithJwt, res, next) => {
|
||||
if (req.shouldCease) {
|
||||
return next("router")
|
||||
}
|
||||
|
||||
if (
|
||||
["6-74", "7-3", "7-17", "8-10"].includes(
|
||||
<string>req.serverVersion,
|
||||
)
|
||||
) {
|
||||
return next() // continue along h3 router
|
||||
}
|
||||
|
||||
next("router")
|
||||
})
|
||||
.use(primaryRouter),
|
||||
)
|
||||
|
||||
app.all("*", (req, res) => {
|
||||
log(LogLevel.WARN, `Unhandled URL: ${req.url}`)
|
||||
res.status(404).send("Not found!")
|
||||
})
|
||||
|
||||
program.description(
|
||||
"The Peacock Project is a HITMAN™ World of Assassination Trilogy server built for general use.",
|
||||
)
|
||||
|
||||
const PEECOCK_ART = `
|
||||
███████████ ██████████ ██████████ █████████ ███████ █████████ █████ ████
|
||||
░░███░░░░░███░░███░░░░░█░░███░░░░░█ ███░░░░░███ ███░░░░░███ ███░░░░░███░░███ ███░
|
||||
░███ ░███ ░███ █ ░ ░███ █ ░ ███ ░░░ ███ ░░███ ███ ░░░ ░███ ███
|
||||
░██████████ ░██████ ░██████ ░███ ░███ ░███░███ ░███████
|
||||
░███░░░░░░ ░███░░█ ░███░░█ ░███ ░███ ░███░███ ░███░░███
|
||||
░███ ░███ ░ █ ░███ ░ █░░███ ███░░███ ███ ░░███ ███ ░███ ░░███
|
||||
█████ ██████████ ██████████ ░░█████████ ░░░███████░ ░░█████████ █████ ░░████
|
||||
░░░░░ ░░░░░░░░░░ ░░░░░░░░░░ ░░░░░░░░░ ░░░░░░░ ░░░░░░░░░ ░░░░░ ░░░░
|
||||
`
|
||||
|
||||
function startServer(options: { hmr: boolean; pluginDevHost: boolean }): void {
|
||||
checkForUpdates()
|
||||
|
||||
if (!IS_LAUNCHER) {
|
||||
console.log(
|
||||
Math.random() < 0.001
|
||||
? PEECOCK_ART
|
||||
: picocolors.greenBright(`
|
||||
███████████ ██████████ █████████ █████████ ███████ █████████ █████ ████
|
||||
░░███░░░░░███░░███░░░░░█ ███░░░░░███ ███░░░░░███ ███░░░░░███ ███░░░░░███░░███ ███░
|
||||
░███ ░███ ░███ █ ░ ░███ ░███ ███ ░░░ ███ ░░███ ███ ░░░ ░███ ███
|
||||
░██████████ ░██████ ░███████████ ░███ ░███ ░███░███ ░███████
|
||||
░███░░░░░░ ░███░░█ ░███░░░░░███ ░███ ░███ ░███░███ ░███░░███
|
||||
░███ ░███ ░ █ ░███ ░███ ░░███ ███░░███ ███ ░░███ ███ ░███ ░░███
|
||||
█████ ██████████ █████ █████ ░░█████████ ░░░███████░ ░░█████████ █████ ░░████
|
||||
░░░░░ ░░░░░░░░░░ ░░░░░ ░░░░░ ░░░░░░░░░ ░░░░░░░ ░░░░░░░░░ ░░░░░ ░░░░
|
||||
`),
|
||||
)
|
||||
}
|
||||
|
||||
log(
|
||||
LogLevel.INFO,
|
||||
`This is Peacock v${PEACOCKVERSTRING} (rev ${PEACOCKVER}), with Node v${process.versions.node}.`,
|
||||
)
|
||||
|
||||
// jokes lol
|
||||
if (getFlag("jokes") === true) {
|
||||
log(
|
||||
LogLevel.INFO,
|
||||
picocolors.yellowBright(
|
||||
`${jokes[random.int(0, jokes.length - 1)]}`,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// make sure required folder structure is in place
|
||||
for (const dir of [
|
||||
"contractSessions",
|
||||
"userdata",
|
||||
"contracts",
|
||||
join("userdata", "epicids"),
|
||||
join("userdata", "steamids"),
|
||||
join("userdata", "users"),
|
||||
join("userdata", "h1", "steamids"),
|
||||
join("userdata", "h1", "epicids"),
|
||||
join("userdata", "h1", "users"),
|
||||
join("userdata", "h2", "steamids"),
|
||||
join("userdata", "h2", "users"),
|
||||
join("userdata", "scpc", "users"),
|
||||
join("userdata", "scpc", "steamids"),
|
||||
join("images", "actors"),
|
||||
]) {
|
||||
if (existsSync(dir)) {
|
||||
continue
|
||||
}
|
||||
|
||||
log(LogLevel.DEBUG, `Creating missing directory ${dir}`)
|
||||
mkdirSync(dir, { recursive: true })
|
||||
}
|
||||
|
||||
if (options.hmr) {
|
||||
log(LogLevel.DEBUG, "Experimental HMR enabled.")
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
setupHotListener("contracts", () => {
|
||||
log(LogLevel.INFO, "Detected a change in contracts! Re-indexing...")
|
||||
controller.index()
|
||||
})
|
||||
}
|
||||
|
||||
// once contracts directory is present, we are clear to boot
|
||||
loadouts.init()
|
||||
controller.boot(options.pluginDevHost)
|
||||
|
||||
const httpServer = http.createServer(app)
|
||||
|
||||
// @ts-expect-error Non-matching method sig
|
||||
httpServer.listen(port, host)
|
||||
log(LogLevel.INFO, "Server started.")
|
||||
|
||||
if (getFlag("discordRp") === true) {
|
||||
initRp()
|
||||
}
|
||||
}
|
||||
|
||||
program.option("--hmr", "enable experimental hot reloading of contracts")
|
||||
program.option(
|
||||
"--plugin-dev-host",
|
||||
"activate plugin development features - requires plugin dev workspace setup",
|
||||
)
|
||||
program.action(startServer)
|
||||
|
||||
program
|
||||
.command("swizzle")
|
||||
.option(
|
||||
"-c, --config <name>",
|
||||
"the config file to generate an override for",
|
||||
"",
|
||||
)
|
||||
.option("--list", "get a list of config files that can be overridden")
|
||||
.description(
|
||||
"generates a file that overrides its internal config counterpart",
|
||||
)
|
||||
.action((args: { list: boolean; config: string }) => {
|
||||
if (args.list) {
|
||||
log(LogLevel.INFO, "The following configurations can be swizzled:")
|
||||
getSwizzleable().forEach((swizzleable) => {
|
||||
log(LogLevel.INFO, ` - ${swizzleable}`)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// doesn't want list, but hasn't specified a swizzleable
|
||||
if (!args.list && args.config === "") {
|
||||
log(LogLevel.ERROR, "No config specified! - Aborting.")
|
||||
return process.exit(1)
|
||||
}
|
||||
|
||||
return swizzle(args.config)
|
||||
})
|
||||
|
||||
program
|
||||
.command("tools")
|
||||
.description("open the tools UI")
|
||||
.action(() => {
|
||||
toolsMenu()
|
||||
})
|
||||
|
||||
program
|
||||
.command("pack")
|
||||
.argument("<input>", "input file to pack")
|
||||
.option("-o, --output <path>", "where to output the packed file to", "")
|
||||
.description("packs an input file into a Challenge Resource Package")
|
||||
.action((input, options: { output: string }) => {
|
||||
const outputPath =
|
||||
options.output !== ""
|
||||
? options.output
|
||||
: input.replace(/\.[^/\\.]+$/, ".crp")
|
||||
|
||||
writeFileSync(
|
||||
outputPath,
|
||||
pack(JSON.parse(readFileSync(input).toString())),
|
||||
)
|
||||
|
||||
log(LogLevel.INFO, `Packed "${input}" to "${outputPath}" successfully.`)
|
||||
})
|
||||
|
||||
program
|
||||
.command("unpack")
|
||||
.argument("<input>", "input file to unpack")
|
||||
.option("-o, --output <path>", "where to output the unpacked file to", "")
|
||||
.description("unpacks a Challenge Resource Package")
|
||||
.action((input, options: { output: string }) => {
|
||||
const outputPath =
|
||||
options.output !== ""
|
||||
? options.output
|
||||
: input.replace(/\.[^/\\.]+$/, ".json")
|
||||
|
||||
writeFileSync(outputPath, JSON.stringify(unpack(readFileSync(input))))
|
||||
|
||||
log(
|
||||
LogLevel.INFO,
|
||||
`Unpacked "${input}" to "${outputPath}" successfully.`,
|
||||
)
|
||||
})
|
||||
|
||||
program.parse(process.argv)
|
262
components/inventory.ts
Normal file
262
components/inventory.ts
Normal file
@ -0,0 +1,262 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 The Peacock Project Team
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { getVersionedConfig } from "./configSwizzleManager"
|
||||
import type { GameVersion, Unlockable } from "./types/types"
|
||||
import {
|
||||
brokenItems,
|
||||
DELUXE_UNLOCKABLES,
|
||||
EXECUTIVE_UNLOCKABLES,
|
||||
H1_GOTY_UNLOCKABLES,
|
||||
H1_REQUIEM_UNLOCKABLES,
|
||||
H2_RACCOON_STINGRAY_UNLOCKABLES,
|
||||
SIN_ENVY_UNLOCKABLES,
|
||||
SIN_GLUTTONY_UNLOCKABLES,
|
||||
SIN_GREED_UNLOCKABLES,
|
||||
SIN_LUST_UNLOCKABLES,
|
||||
SIN_PRIDE_UNLOCKABLES,
|
||||
SIN_SLOTH_UNLOCKABLES,
|
||||
SIN_WRATH_UNLOCKABLES,
|
||||
TRINITY_UNLOCKABLES,
|
||||
WINTERSPORTS_UNLOCKABLES,
|
||||
} from "./ownership"
|
||||
import { EPIC_NAMESPACE_2016 } from "./platformEntitlements"
|
||||
|
||||
/**
|
||||
* An inventory item.
|
||||
*/
|
||||
export interface InventoryItem {
|
||||
InstanceId: string
|
||||
ProfileId: string
|
||||
Unlockable: Unlockable
|
||||
Properties: Record<string, string>
|
||||
}
|
||||
|
||||
const inventoryUserCache: Map<string, InventoryItem[]> = new Map()
|
||||
|
||||
/**
|
||||
* Clears a user's inventory.
|
||||
*
|
||||
* @param userId The user's ID.
|
||||
*/
|
||||
export function clearInventoryFor(userId: string): void {
|
||||
inventoryUserCache.delete(userId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the entire inventory cache.
|
||||
*/
|
||||
export function clearInventoryCache(): void {
|
||||
inventoryUserCache.clear()
|
||||
}
|
||||
|
||||
export function createInventory(
|
||||
profileId: string,
|
||||
gameVersion: GameVersion,
|
||||
entP: string[],
|
||||
): InventoryItem[] {
|
||||
if (inventoryUserCache.has(profileId)) {
|
||||
return inventoryUserCache.get(profileId)!
|
||||
}
|
||||
|
||||
// add all unlockables to player's inventory
|
||||
const allunlockables = getVersionedConfig<Unlockable[]>(
|
||||
"allunlockables",
|
||||
gameVersion,
|
||||
true,
|
||||
).filter((u) => u.Type !== "location") // locations not in inventory
|
||||
|
||||
// ts-expect-error It cannot be undefined.
|
||||
const filtered: InventoryItem[] = allunlockables
|
||||
.map((unlockable) => {
|
||||
if (brokenItems.includes(unlockable.Guid)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (unlockable.Guid === "1efe1010-4fff-4ee2-833e-7c58b6518e3e") {
|
||||
unlockable.Properties.Name =
|
||||
"char_reward_hero_halloweenoutfit_m_pro140008_name_ebf1e362-671f-47e8-8c88-dd490d8ad866"
|
||||
unlockable.Properties.Description =
|
||||
"char_reward_hero_halloweenoutfit_m_pro140008_description_ebf1e362-671f-47e8-8c88-dd490d8ad866"
|
||||
}
|
||||
|
||||
unlockable.GameAsset = null
|
||||
unlockable.DisplayNameLocKey = `UI_${unlockable.Id}_NAME`
|
||||
return {
|
||||
InstanceId: unlockable.Guid,
|
||||
ProfileId: profileId,
|
||||
Unlockable: unlockable,
|
||||
Properties: {},
|
||||
}
|
||||
})
|
||||
// filter again, this time removing legacy unlockables
|
||||
.filter((unlockContainer) => {
|
||||
if (!unlockContainer) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (gameVersion === "h1") {
|
||||
return true
|
||||
}
|
||||
|
||||
const e = entP
|
||||
const { Id: id } = unlockContainer!.Unlockable
|
||||
|
||||
if (!e) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (gameVersion === "h3") {
|
||||
if (WINTERSPORTS_UNLOCKABLES.includes(id)) {
|
||||
return (
|
||||
e.includes("afa4b921503f43339c360d4b53910791") ||
|
||||
e.includes("1829590")
|
||||
)
|
||||
}
|
||||
|
||||
if (EXECUTIVE_UNLOCKABLES.includes(id)) {
|
||||
return (
|
||||
e.includes("6408de14f7dc46b9a33adcf6cbc4d159") ||
|
||||
e.includes("afa4b921503f43339c360d4b53910791") ||
|
||||
e.includes("1829590")
|
||||
)
|
||||
}
|
||||
|
||||
if (H1_REQUIEM_UNLOCKABLES.includes(id)) {
|
||||
return (
|
||||
e.includes("e698e1a4b63947b0bc9349a5ae2dc015") ||
|
||||
e.includes("1843460")
|
||||
)
|
||||
}
|
||||
|
||||
if (H1_GOTY_UNLOCKABLES.includes(id)) {
|
||||
return (
|
||||
e.includes("894d1e6771044f48a8fdde934b8e443a") ||
|
||||
e.includes("1843460") ||
|
||||
e.includes("1829595")
|
||||
)
|
||||
}
|
||||
|
||||
if (H2_RACCOON_STINGRAY_UNLOCKABLES.includes(id)) {
|
||||
return (
|
||||
e.includes("afa4b921503f43339c360d4b53910791") ||
|
||||
e.includes("1829590")
|
||||
)
|
||||
}
|
||||
} else if (gameVersion === "h2") {
|
||||
if (WINTERSPORTS_UNLOCKABLES.includes(id)) {
|
||||
return e.includes("957693")
|
||||
}
|
||||
} else if (
|
||||
// @ts-expect-error The types do actually overlap, but there is no way to show that.
|
||||
gameVersion === "h1" &&
|
||||
(e.includes("0a73eaedcac84bd28b567dbec764c5cb") ||
|
||||
e.includes(EPIC_NAMESPACE_2016))
|
||||
) {
|
||||
// h1 EGS
|
||||
if (
|
||||
H1_REQUIEM_UNLOCKABLES.includes(id) ||
|
||||
H1_GOTY_UNLOCKABLES.includes(id)
|
||||
) {
|
||||
return e.includes("81aecb49a60b47478e61e1cbd68d63c5")
|
||||
}
|
||||
}
|
||||
|
||||
if (DELUXE_UNLOCKABLES.includes(id)) {
|
||||
return (
|
||||
e.includes("bc610b36c75442299edcbe99f6f0fb60") ||
|
||||
e.includes("1829591")
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
TODO: Fix this entitlement check (confirmed its broken with Blazer)
|
||||
if (LEGACY_UNLOCKABLES.includes(id)) {
|
||||
return (
|
||||
e.includes("0b59243cb8aa420691b66be1ecbe68c0") ||
|
||||
e.includes("1829593")
|
||||
)
|
||||
}
|
||||
*/
|
||||
|
||||
if (SIN_GREED_UNLOCKABLES.includes(id)) {
|
||||
return (
|
||||
e.includes("0e8632b4cdfb415e94291d97d727b98d") ||
|
||||
e.includes("1829580")
|
||||
)
|
||||
}
|
||||
|
||||
if (SIN_PRIDE_UNLOCKABLES.includes(id)) {
|
||||
return (
|
||||
e.includes("3f9adc216dde44dda5e829f11740a0a2") ||
|
||||
e.includes("1829581")
|
||||
)
|
||||
}
|
||||
|
||||
if (SIN_SLOTH_UNLOCKABLES.includes(id)) {
|
||||
return (
|
||||
e.includes("aece009ff59441c0b526f8aa69e24cfb") ||
|
||||
e.includes("1829582")
|
||||
)
|
||||
}
|
||||
|
||||
if (SIN_LUST_UNLOCKABLES.includes(id)) {
|
||||
return (
|
||||
e.includes("dfe5aeb89976450ba1e0e2c208b63d33") ||
|
||||
e.includes("1829583")
|
||||
)
|
||||
}
|
||||
|
||||
if (SIN_GLUTTONY_UNLOCKABLES.includes(id)) {
|
||||
return (
|
||||
e.includes("30107bff80024d1ab291f9cd3bac9fac") ||
|
||||
e.includes("1829584")
|
||||
)
|
||||
}
|
||||
|
||||
if (SIN_ENVY_UNLOCKABLES.includes(id)) {
|
||||
return (
|
||||
e.includes("0403062df0d347619c8dcf043c65c02e") ||
|
||||
e.includes("1829585")
|
||||
)
|
||||
}
|
||||
|
||||
if (SIN_WRATH_UNLOCKABLES.includes(id)) {
|
||||
return (
|
||||
e.includes("9e936ed2507a473db6f53ad24d2da587") ||
|
||||
e.includes("1829586")
|
||||
)
|
||||
}
|
||||
|
||||
if (TRINITY_UNLOCKABLES.includes(id)) {
|
||||
return (
|
||||
e.includes("5d06a6c6af9b4875b3530d5328f61287") ||
|
||||
e.includes("1829596")
|
||||
)
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
for (const unlockable of filtered) {
|
||||
unlockable!.ProfileId = profileId
|
||||
}
|
||||
|
||||
inventoryUserCache.set(profileId, filtered)
|
||||
return filtered
|
||||
}
|
379
components/livesplit/liveSplitClient.ts
Normal file
379
components/livesplit/liveSplitClient.ts
Normal file
@ -0,0 +1,379 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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 { Socket } from "net"
|
||||
import { EventEmitter } from "events"
|
||||
|
||||
export type LiveSplitResult = Promise<string | boolean | undefined>
|
||||
|
||||
/**
|
||||
* Node.js client for the LiveSplit Server running instance.
|
||||
*
|
||||
* @see https://github.com/LiveSplit/LiveSplit.Server LiveSplit server component
|
||||
* @see https://github.com/satanch/livesplit-node-client Original source code
|
||||
* @author satanch (https://github.com/satanch)
|
||||
* @license MIT
|
||||
*/
|
||||
export class LiveSplitClient extends EventEmitter {
|
||||
timeout: number
|
||||
private readonly _connectionDetails: {
|
||||
ip: string
|
||||
port: number
|
||||
}
|
||||
private _connected: boolean
|
||||
private _initGameTimeOnce: boolean
|
||||
private _socket: Socket | undefined
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
*
|
||||
* @param address - Connection address, in the format of 127.0.0.1:1234.
|
||||
*/
|
||||
constructor(address: string) {
|
||||
super()
|
||||
|
||||
const formatted: string[] = address.split(":")
|
||||
|
||||
if (formatted.length !== 2) {
|
||||
throw new Error(
|
||||
"Failed to parse connection details! IP:PORT expected.",
|
||||
)
|
||||
}
|
||||
|
||||
this._connectionDetails = {
|
||||
ip: formatted[0],
|
||||
port: parseInt(formatted[1]),
|
||||
}
|
||||
|
||||
this._connected = false
|
||||
this.timeout = 100
|
||||
|
||||
/*
|
||||
According to: https://github.com/LiveSplit/LiveSplit.Server/blob/a4a57716dce90936606bfc8f8ac84f7623773aa5/README.md#commands
|
||||
|
||||
When using Game Time, it's important that you call "initgametime" once. Once "initgametime" is used, an additional comparison will appear, and you can switch to it via the context menu (Compare Against > Game Time). This special comparison will show everything based on the Game Time (every component now shows Game Time based information).
|
||||
*/
|
||||
this._initGameTimeOnce = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that no disallowed symbols are present in the command.
|
||||
*
|
||||
* @param str The command.
|
||||
* @throws {Error} If the command includes `\r\n`.
|
||||
*/
|
||||
private static _checkDisallowedSymbols(str: string): void {
|
||||
if (str.indexOf("\r\n") !== -1) {
|
||||
throw new Error("No newline symbols allowed!")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs connection attempt to the LiveSplit Server instance.
|
||||
*/
|
||||
connect(): Promise<boolean> {
|
||||
this._socket = new Socket()
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this._socket!.connect(
|
||||
this._connectionDetails.port,
|
||||
this._connectionDetails.ip,
|
||||
() => {
|
||||
this._connected = true
|
||||
this.emit("connected")
|
||||
resolve(this._connected)
|
||||
},
|
||||
)
|
||||
|
||||
this._socket!.on("data", (data) => {
|
||||
// noinspection TypeScriptValidateJSTypes
|
||||
this.emit("data", data.toString("utf-8").replace("\r\n", ""))
|
||||
})
|
||||
|
||||
this._socket!.on("error", (err) => {
|
||||
reject(err)
|
||||
})
|
||||
|
||||
this._socket!.on("close", () => {
|
||||
this._connected = false
|
||||
this.emit("disconnected")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect client from the server.
|
||||
*/
|
||||
disconnect(): boolean {
|
||||
if (!this._connected) {
|
||||
return false
|
||||
}
|
||||
|
||||
this._socket?.destroy()
|
||||
this._connected = false
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Send command to the LiveSplit Server instance.
|
||||
*/
|
||||
async send(command: string, expectResponse = true): LiveSplitResult {
|
||||
if (!this._connected) {
|
||||
throw new Error("Client must be connected to the server!")
|
||||
}
|
||||
|
||||
LiveSplitClient._checkDisallowedSymbols(command)
|
||||
|
||||
this._socket?.write(`${command}\r\n`)
|
||||
|
||||
if (expectResponse) {
|
||||
return await this._waitForResponse()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the timer.
|
||||
*/
|
||||
async startTimer(): LiveSplitResult {
|
||||
return await this.send("starttimer", false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Start or split.
|
||||
*/
|
||||
async startOrSplit(): LiveSplitResult {
|
||||
return await this.send("startorsplit", false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Split.
|
||||
*/
|
||||
async split(): LiveSplitResult {
|
||||
return await this.send("split", false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsplit.
|
||||
*/
|
||||
async unsplit(): LiveSplitResult {
|
||||
return await this.send("unsplit", false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip split.
|
||||
*/
|
||||
async skipSplit(): LiveSplitResult {
|
||||
return await this.send("skipsplit", false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause.
|
||||
*/
|
||||
async pause(): LiveSplitResult {
|
||||
return await this.send("pause", false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume.
|
||||
*/
|
||||
async resume(): LiveSplitResult {
|
||||
return await this.send("resume", false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset.
|
||||
*/
|
||||
async reset(): LiveSplitResult {
|
||||
return await this.send("reset", false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Init game time. Can only be called once according to LiveSplit Server documentation.
|
||||
*/
|
||||
async initGameTime(): LiveSplitResult {
|
||||
if (this._initGameTimeOnce) {
|
||||
return false
|
||||
}
|
||||
|
||||
this._initGameTimeOnce = true
|
||||
return await this.send("initgametime", false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set game time.
|
||||
*
|
||||
* @param time Game time.
|
||||
*/
|
||||
async setGameTime(time: string): LiveSplitResult {
|
||||
return await this.send(`setgametime ${time}`, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set loading times.
|
||||
*
|
||||
* @param time Loading times.
|
||||
*/
|
||||
async setLoadingTimes(time: string): LiveSplitResult {
|
||||
return await this.send(`setloadingtimes ${time}`, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause game time.
|
||||
*/
|
||||
async pauseGameTime(): LiveSplitResult {
|
||||
return await this.send("pausegametime", false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpause game time.
|
||||
*/
|
||||
async unpauseGameTime(): LiveSplitResult {
|
||||
return await this.send("unpausegametime", false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set comparison.
|
||||
*
|
||||
* @param comparison The comparison.
|
||||
*/
|
||||
async setComparison(comparison: string): LiveSplitResult {
|
||||
return await this.send(`setcomparison ${comparison}`, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get delta.
|
||||
*
|
||||
* @param comparison The comparison.
|
||||
*/
|
||||
async getDelta(comparison = ""): LiveSplitResult {
|
||||
if (comparison.length > 0) {
|
||||
comparison = ` ${comparison}`
|
||||
}
|
||||
|
||||
return await this.send(`getdelta${comparison}`, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last split time.
|
||||
*/
|
||||
async getLastSplitTime(): LiveSplitResult {
|
||||
return await this.send("getlastsplittime", true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get comparison split time.
|
||||
*/
|
||||
async getComparisonSplitTime(): LiveSplitResult {
|
||||
return await this.send("getcomparisonsplittime", true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current time.
|
||||
*/
|
||||
async getCurrentTime(): LiveSplitResult {
|
||||
return await this.send("getcurrenttime", true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the final time.
|
||||
*
|
||||
* @param comparison The comparison.
|
||||
*/
|
||||
async getFinalTime(comparison = ""): LiveSplitResult {
|
||||
if (comparison.length > 0) {
|
||||
comparison = ` ${comparison}`
|
||||
}
|
||||
|
||||
return await this.send(`getfinaltime${comparison}`, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get predicted time.
|
||||
*
|
||||
* @param comparison The comparison.
|
||||
*/
|
||||
async getPredictedTime(comparison: string): LiveSplitResult {
|
||||
return await this.send(`getpredictedtime ${comparison}`, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the best possible time.
|
||||
*/
|
||||
async getBestPossibleTime(): LiveSplitResult {
|
||||
return await this.send("getbestpossibletime", true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get split index.
|
||||
*/
|
||||
async getSplitIndex(): LiveSplitResult {
|
||||
return await this.send("getsplitindex", true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current split name.
|
||||
*/
|
||||
async getCurrentSplitName(): LiveSplitResult {
|
||||
return await this.send("getcurrentsplitname", true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get previous split name.
|
||||
*/
|
||||
async getPreviousSplitname(): LiveSplitResult {
|
||||
return await this.send("getprevioussplitname", true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current timer phase.
|
||||
*/
|
||||
async getCurrentTimerPhase(): LiveSplitResult {
|
||||
return await this.send("getcurrenttimerphase", true)
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
private async _waitForResponse(): LiveSplitResult {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let listener: ((...args: any[]) => void) | undefined = undefined
|
||||
|
||||
const responseRecieved = new Promise<Awaited<LiveSplitResult>>(
|
||||
(resolve) => {
|
||||
listener = (data) => {
|
||||
resolve(data)
|
||||
}
|
||||
|
||||
this.once("data", listener)
|
||||
},
|
||||
)
|
||||
|
||||
const responseTimeout = new Promise<Awaited<LiveSplitResult>>(
|
||||
(resolve) => {
|
||||
setTimeout(() => {
|
||||
this.removeListener("data", listener)
|
||||
resolve(undefined)
|
||||
}, this.timeout)
|
||||
},
|
||||
)
|
||||
|
||||
return await Promise.race([responseRecieved, responseTimeout])
|
||||
}
|
||||
}
|
446
components/livesplit/liveSplitManager.ts
Normal file
446
components/livesplit/liveSplitManager.ts
Normal file
@ -0,0 +1,446 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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 { LiveSplitClient, LiveSplitResult } from "./liveSplitClient"
|
||||
import { log, LogLevel } from "../loggingInterop"
|
||||
import { getAllCampaigns } from "../menus/campaigns"
|
||||
import { Campaign, GameVersion, IHit, Seconds, StoryData } from "../types/types"
|
||||
import { getFlag } from "../flags"
|
||||
|
||||
export class LiveSplitManager {
|
||||
private readonly _liveSplitClient: LiveSplitClient
|
||||
// https://youtrack.jetbrains.com/issue/WEB-54745
|
||||
// noinspection TypeScriptFieldCanBeMadeReadonly
|
||||
private _initialized: boolean
|
||||
private _initializationAttempted: boolean
|
||||
private _resetMinimum: Seconds
|
||||
private _currentCampaign: string[]
|
||||
private _inValidCampaignRun: boolean
|
||||
private _currentMission: string | undefined
|
||||
private _currentMissionTotalTime: number
|
||||
private _campaignTotalTime: number
|
||||
private _completedMissions: string[]
|
||||
private _raceMode: boolean | undefined // gets late-initialized, use _isRaceMode to access
|
||||
|
||||
constructor() {
|
||||
this._initialized = false
|
||||
this._initializationAttempted = false
|
||||
this._resetMinimum = 1
|
||||
this._currentMission = undefined
|
||||
this._inValidCampaignRun = false
|
||||
this._completedMissions = []
|
||||
this._currentMissionTotalTime = 0
|
||||
this._campaignTotalTime = 0
|
||||
this._raceMode = undefined
|
||||
this._liveSplitClient = new LiveSplitClient("127.0.0.1:16834")
|
||||
this.init()
|
||||
.then(() => {
|
||||
this._initialized = true
|
||||
this._initializationAttempted = true
|
||||
return
|
||||
})
|
||||
.catch((e) => {
|
||||
log(LogLevel.DEBUG, "Failed to initialize LiveSplit: ")
|
||||
log(LogLevel.DEBUG, e)
|
||||
this._initializationAttempted = true
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* PUBLIC INTERFACE
|
||||
*/
|
||||
|
||||
missionIntentResolved(contractId: string, startId: string): void {
|
||||
if (
|
||||
LiveSplitManager._isClub27(contractId) &&
|
||||
LiveSplitManager._isBangkokDefaultStartLocation(startId)
|
||||
) {
|
||||
this._resetMinimum = 10
|
||||
return
|
||||
}
|
||||
|
||||
this._resetMinimum = 0
|
||||
}
|
||||
|
||||
async startMission(
|
||||
contractId: string,
|
||||
gameVersion: GameVersion,
|
||||
userId: string,
|
||||
) {
|
||||
if (!this._checkInit()) {
|
||||
return
|
||||
}
|
||||
|
||||
const campaign = getCampaignMissions(contractId, gameVersion, userId)
|
||||
if (campaign === undefined) {
|
||||
this._invalidateRun(contractId)
|
||||
return
|
||||
}
|
||||
this._currentCampaign = campaign
|
||||
|
||||
const isStartOfCampaign =
|
||||
this._currentCampaign.indexOf(contractId) === 0
|
||||
|
||||
if (isStartOfCampaign) {
|
||||
this._currentMission = contractId
|
||||
await this._resetCampaign()
|
||||
await this._pushGameTime()
|
||||
return
|
||||
}
|
||||
|
||||
if (this._inValidCampaignRun) {
|
||||
const numComplete = this._completedMissions.length
|
||||
if (contractId === this._currentMission) {
|
||||
const lastCompleted = this._completedMissions[numComplete - 1]
|
||||
|
||||
if (contractId === lastCompleted) {
|
||||
// for whatever reason, previous mission completed but still resetting,
|
||||
// un-split. total time is not reset on mission complete, so it should still be valid.
|
||||
// do pop the completed mission though as we're entering a new attempt
|
||||
this._completedMissions.pop()
|
||||
if (!this._isRaceMode) {
|
||||
logLiveSplitError(
|
||||
await this._liveSplitClient.unsplit(),
|
||||
"unsplit",
|
||||
)
|
||||
logLiveSplitError(
|
||||
await this._liveSplitClient.startTimer(),
|
||||
"startTimer",
|
||||
)
|
||||
logLiveSplitError(
|
||||
await this._liveSplitClient.pauseGameTime(),
|
||||
"pauseGameTime",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
await this._pushGameTime()
|
||||
|
||||
// was a reset, don't need to do anything more
|
||||
return
|
||||
}
|
||||
|
||||
// we've started a new mission
|
||||
|
||||
// figure out what the current mission should be based on the active campaign and
|
||||
// the previous completed mission
|
||||
const nextCampaignMission = this._currentCampaign[numComplete]
|
||||
|
||||
if (contractId === nextCampaignMission) {
|
||||
// if it does match, we're on a valid next mission so reset the state
|
||||
this._currentMission = contractId
|
||||
this._currentMissionTotalTime = 0
|
||||
await this._pushGameTime()
|
||||
|
||||
if (!this._isRaceMode) {
|
||||
logLiveSplitError(
|
||||
await this._liveSplitClient.startTimer(),
|
||||
"startTimer",
|
||||
)
|
||||
logLiveSplitError(
|
||||
await this._liveSplitClient.pauseGameTime(),
|
||||
"pauseGameTime",
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// if it doesn't match, invalidate the current run
|
||||
this._invalidateRun(contractId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async failMission(attemptTime: Seconds) {
|
||||
if (!this._checkInit()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this._inValidCampaignRun) {
|
||||
this._addMissionTime(attemptTime)
|
||||
LiveSplitManager._logAttempt(attemptTime)
|
||||
await this._pushGameTime()
|
||||
}
|
||||
}
|
||||
|
||||
async completeMission(attemptTime: Seconds) {
|
||||
LiveSplitManager._logAttempt(attemptTime)
|
||||
|
||||
if (!this._checkInit()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this._inValidCampaignRun) {
|
||||
this._addMissionTime(attemptTime)
|
||||
log(
|
||||
LogLevel.INFO,
|
||||
`Total mission time with resets: ${this._currentMissionTotalTime}`,
|
||||
)
|
||||
this._completedMissions.push(this._currentMission)
|
||||
await this._pushGameTime()
|
||||
if (this._isRaceMode) {
|
||||
if (
|
||||
this._completedMissions.length ===
|
||||
this._currentCampaign.length
|
||||
) {
|
||||
// Campaign is complete, racetimegg in livesplit uses first split as completion
|
||||
logLiveSplitError(
|
||||
await this._liveSplitClient.split(),
|
||||
"split",
|
||||
)
|
||||
logLiveSplitError(
|
||||
await this._liveSplitClient.pause(),
|
||||
"pause",
|
||||
)
|
||||
|
||||
const flooredTime = Math.floor(this._campaignTotalTime)
|
||||
const minutes = Math.floor(flooredTime / 60)
|
||||
const seconds = Math.floor(flooredTime % 60)
|
||||
|
||||
log(
|
||||
LogLevel.INFO,
|
||||
`Total campaign in-game time with resets: ${minutes}:${seconds}`,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
logLiveSplitError(await this._liveSplitClient.split(), "split")
|
||||
if (
|
||||
this._completedMissions.length ===
|
||||
this._currentCampaign.length
|
||||
) {
|
||||
// Pause real time timer because campaign is potentially complete
|
||||
logLiveSplitError(
|
||||
await this._liveSplitClient.pause(),
|
||||
"pause",
|
||||
)
|
||||
}
|
||||
}
|
||||
// purposely do not reset this._currentMissionTotalTime yet in case mission complete
|
||||
// but runner still wants to reset (could happen if lose SA at the end of a fast map like Dubai)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* PRIVATE METHODS
|
||||
*/
|
||||
|
||||
private async init() {
|
||||
logLiveSplitError(await this._liveSplitClient.connect(), "connect")
|
||||
logLiveSplitError(
|
||||
await this._liveSplitClient.initGameTime(),
|
||||
"initGameTime",
|
||||
)
|
||||
logLiveSplitError(
|
||||
await this._liveSplitClient.pauseGameTime(),
|
||||
"pauseGameTime",
|
||||
)
|
||||
}
|
||||
|
||||
private _checkInit(): boolean {
|
||||
if (!this._initializationAttempted) {
|
||||
log(
|
||||
LogLevel.ERROR,
|
||||
"Tried to perform LiveSplit action before LiveSplit initialization finished.",
|
||||
)
|
||||
}
|
||||
|
||||
return this._initialized
|
||||
}
|
||||
|
||||
private static _logAttempt(attemptTime: number): void {
|
||||
log(
|
||||
LogLevel.INFO,
|
||||
`Time (with milliseconds): ${Math.floor(
|
||||
attemptTime,
|
||||
)} (${attemptTime})`,
|
||||
)
|
||||
}
|
||||
|
||||
private get _isRaceMode(): boolean {
|
||||
if (this._raceMode === undefined) {
|
||||
this._raceMode = getFlag("autoSplitterRacetimegg") === true
|
||||
}
|
||||
|
||||
return this._raceMode as boolean
|
||||
}
|
||||
|
||||
private async _invalidateRun(contractId: string) {
|
||||
log(
|
||||
LogLevel.INFO,
|
||||
"Entered invalid mission for current campaign state, invalidating autosplitter run. Start first mission of a campaign to reset and start a new run.",
|
||||
)
|
||||
const nextCampaignMission =
|
||||
this._currentCampaign[this._completedMissions.length]
|
||||
log(
|
||||
LogLevel.INFO,
|
||||
"Detected campaign missions: " + this._currentCampaign,
|
||||
)
|
||||
log(LogLevel.INFO, "Completed missions: " + this._completedMissions)
|
||||
log(
|
||||
LogLevel.INFO,
|
||||
"Next campaign mission detected: " + nextCampaignMission,
|
||||
)
|
||||
log(LogLevel.INFO, "Attempted to start mission: " + contractId)
|
||||
this._inValidCampaignRun = false
|
||||
|
||||
// if we're in race mode, we don't pause the timer because the user can just manually complete the run if they need to
|
||||
// and verification will take care of the validity
|
||||
if (!this._isRaceMode) {
|
||||
logLiveSplitError(await this._liveSplitClient.pause(), "pause")
|
||||
}
|
||||
}
|
||||
|
||||
private async _resetCampaign() {
|
||||
log(LogLevel.INFO, "Starting new autosplitter campaign.")
|
||||
log(
|
||||
LogLevel.INFO,
|
||||
`Detected campaign missions: ${this._currentCampaign}`,
|
||||
)
|
||||
this._completedMissions = []
|
||||
this._inValidCampaignRun = true
|
||||
this._currentMissionTotalTime = 0
|
||||
this._campaignTotalTime = 0
|
||||
|
||||
// race mode does this automatically with racetimegg integration
|
||||
if (!this._isRaceMode) {
|
||||
logLiveSplitError(await this._liveSplitClient.reset(), "reset")
|
||||
logLiveSplitError(
|
||||
await this._liveSplitClient.startTimer(),
|
||||
"startTimer",
|
||||
)
|
||||
logLiveSplitError(
|
||||
await this._liveSplitClient.pauseGameTime(),
|
||||
"pauseGameTime",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private static _isClub27(contractId: string): boolean {
|
||||
return [
|
||||
"db341d9f-58a4-411d-be57-0bc4ed85646b",
|
||||
"ad5f9051-045d-4b8e-8a4d-d84429f467f8",
|
||||
].includes(contractId)
|
||||
}
|
||||
|
||||
private static _isBangkokDefaultStartLocation(
|
||||
startLocationId: string,
|
||||
): boolean {
|
||||
return [
|
||||
"9ddbd515-2519-4c16-98aa-0f87af5d8ef5",
|
||||
// maybe more?
|
||||
].includes(startLocationId)
|
||||
}
|
||||
|
||||
private async _setGameTime(totalTime: Seconds) {
|
||||
// IMPORTANT to floor to int before sending to livesplit or else parsing will fail silently...
|
||||
const flooredTime = Math.floor(totalTime)
|
||||
const minutes = Math.floor(flooredTime / 60)
|
||||
const seconds = Math.floor(flooredTime % 60)
|
||||
|
||||
logLiveSplitError(
|
||||
await this._liveSplitClient.pauseGameTime(),
|
||||
"pauseGameTime",
|
||||
)
|
||||
logLiveSplitError(
|
||||
await this._liveSplitClient.setGameTime(`${minutes}:${seconds}.00`),
|
||||
"setGameTime",
|
||||
)
|
||||
}
|
||||
|
||||
private _addMissionTime(time: Seconds) {
|
||||
// always add at least minimum
|
||||
if (time <= this._resetMinimum) {
|
||||
this._currentMissionTotalTime += this._resetMinimum
|
||||
this._campaignTotalTime += this._resetMinimum
|
||||
} else if (time > 0 && time <= 1) {
|
||||
// if in game time is between 0 and 1, add full second
|
||||
this._currentMissionTotalTime += 1
|
||||
this._campaignTotalTime += 1
|
||||
} else {
|
||||
// important to always floor before adding time
|
||||
this._currentMissionTotalTime += Math.floor(time)
|
||||
this._campaignTotalTime += Math.floor(time)
|
||||
}
|
||||
}
|
||||
|
||||
private async _pushGameTime() {
|
||||
if (!this._initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
await this._setGameTime(this._campaignTotalTime)
|
||||
}
|
||||
}
|
||||
|
||||
function logLiveSplitError(
|
||||
result: Awaited<LiveSplitResult>,
|
||||
failureCall: string,
|
||||
) {
|
||||
if (!result) {
|
||||
log(
|
||||
LogLevel.ERROR,
|
||||
"LiveSplit Server internal error on: " + failureCall,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function getCampaignMissions(
|
||||
missionId: string,
|
||||
gameVersion: GameVersion,
|
||||
userId: string,
|
||||
): string[] | undefined {
|
||||
// find the campaign this mission is in
|
||||
const allCampaigns = getAllCampaigns(gameVersion, userId)
|
||||
|
||||
const findNamedCampaign = (name: string): Campaign | undefined => {
|
||||
return allCampaigns.find((campaign) => campaign.Name === name)
|
||||
}
|
||||
|
||||
const campaignMissionIds = (campaignStoryData: StoryData[]): string[] => {
|
||||
return campaignStoryData
|
||||
.filter((data) => data.Type === "Mission")
|
||||
.map((sd) => (sd.Data as IHit).Id)
|
||||
}
|
||||
|
||||
// the trilogy is the only place where multiple campaigns are merged together
|
||||
const trilogy: string[] = campaignMissionIds([
|
||||
...(findNamedCampaign("UI_SEASON_1")?.StoryData ?? []),
|
||||
...(findNamedCampaign("UI_SEASON_2")?.StoryData ?? []),
|
||||
...(findNamedCampaign("UI_SEASON_3")?.StoryData ?? []),
|
||||
])
|
||||
|
||||
if (trilogy.indexOf(missionId) === -1) {
|
||||
// not in the trilogy, fall back to all campaigns
|
||||
return allCampaigns
|
||||
.map((c) => campaignMissionIds(c.StoryData ?? []))
|
||||
.find((mc) => mc.includes(missionId))
|
||||
} else {
|
||||
const campaignFlag = getFlag("autoSplitterCampaign") as string | number
|
||||
|
||||
switch (campaignFlag) {
|
||||
case 1:
|
||||
return trilogy.slice(0, 6)
|
||||
case 2:
|
||||
return trilogy.slice(6, 14)
|
||||
case 3:
|
||||
return trilogy.slice(14, trilogy.length)
|
||||
default:
|
||||
return trilogy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const liveSplitManager = new LiveSplitManager()
|
277
components/loadouts.ts
Normal file
277
components/loadouts.ts
Normal file
@ -0,0 +1,277 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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 { existsSync, readFileSync, writeFileSync } from "fs"
|
||||
import type {
|
||||
GameVersion,
|
||||
Loadout,
|
||||
LoadoutFile,
|
||||
LoadoutsGameVersion,
|
||||
} from "./types/types"
|
||||
import { Request, Router } from "express"
|
||||
import { json as jsonMiddleware } from "body-parser"
|
||||
import { writeFile } from "atomically"
|
||||
import { nanoid } from "nanoid"
|
||||
|
||||
const LOADOUT_PROFILES_FILE = "userdata/users/lop.json"
|
||||
|
||||
const defaultValue: LoadoutFile = {
|
||||
h1: {
|
||||
selected: null,
|
||||
loadouts: [],
|
||||
},
|
||||
h2: {
|
||||
selected: null,
|
||||
loadouts: [],
|
||||
},
|
||||
h3: {
|
||||
selected: null,
|
||||
loadouts: [],
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* A class for managing loadouts.
|
||||
*/
|
||||
export class Loadouts {
|
||||
private _loadouts: LoadoutFile
|
||||
|
||||
/**
|
||||
* Creates a new instance of the class.
|
||||
*/
|
||||
public constructor() {
|
||||
this._loadouts = undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the loadouts data.
|
||||
*
|
||||
* @returns The loadouts data.
|
||||
*/
|
||||
public get loadouts(): LoadoutFile {
|
||||
return this._loadouts
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutate the current LoadoutFile object.
|
||||
*
|
||||
* @internal Intended for internal use only.
|
||||
* @param newValue The object after the mutation.
|
||||
*/
|
||||
set loadouts(newValue: LoadoutFile) {
|
||||
this._loadouts = newValue
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the loadouts manager.
|
||||
*/
|
||||
public init(): void {
|
||||
if (!existsSync(LOADOUT_PROFILES_FILE)) {
|
||||
this._loadouts = defaultValue
|
||||
|
||||
writeFileSync(LOADOUT_PROFILES_FILE, JSON.stringify(defaultValue))
|
||||
return
|
||||
}
|
||||
|
||||
this._loadouts = JSON.parse(
|
||||
readFileSync(LOADOUT_PROFILES_FILE).toString(),
|
||||
)
|
||||
|
||||
let dirty = false
|
||||
|
||||
// make sure they all have IDs
|
||||
for (const gameVersion of ["h1", "h2", "h3"]) {
|
||||
for (const loadout of this._loadouts[gameVersion].loadouts) {
|
||||
if (!loadout.id) {
|
||||
dirty = true
|
||||
loadout.id = nanoid()
|
||||
}
|
||||
}
|
||||
|
||||
// if the selected value is null/undefined or is not length 0 or 21, it's not a valid id
|
||||
if (
|
||||
!this._loadouts[gameVersion].selected ||
|
||||
![0, 21].includes(this._loadouts[gameVersion].selected.length)
|
||||
) {
|
||||
dirty = true
|
||||
|
||||
// long story short: find a loadout with a name matching the selected value,
|
||||
// and if found, set selected to the id
|
||||
this._loadouts[gameVersion].selected =
|
||||
this._loadouts[gameVersion].loadouts.find(
|
||||
(lo) =>
|
||||
lo.name === this._loadouts[gameVersion].selected,
|
||||
)?.id || ""
|
||||
}
|
||||
}
|
||||
|
||||
if (dirty === true) {
|
||||
writeFileSync(LOADOUT_PROFILES_FILE, JSON.stringify(this._loadouts))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the default loadout (or just a new loadout) for the specified game version, and optionally with a name.
|
||||
*
|
||||
* @param gameVersion The game version to perform the operation on.
|
||||
* @param name The optional name for the new loadout set, defaults to "Unnamed loadout set".
|
||||
* @returns The Loadout object.
|
||||
*/
|
||||
public createDefault(
|
||||
gameVersion: GameVersion,
|
||||
name = "Unnamed loadout set",
|
||||
): Loadout {
|
||||
if (gameVersion === "scpc") {
|
||||
gameVersion = "h1"
|
||||
}
|
||||
|
||||
const l: Loadout = {
|
||||
name,
|
||||
id: nanoid(),
|
||||
data: {},
|
||||
}
|
||||
|
||||
this._loadouts[gameVersion].loadouts.push(l)
|
||||
this._loadouts[gameVersion].selected = l.id
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the active loadout profile for the specified game version. May be undefined.
|
||||
*
|
||||
* @param gameVersion The game version.
|
||||
* @returns The loadout profile or undefined if one isn't selected or none exist.
|
||||
*/
|
||||
public getLoadoutFor(gameVersion: GameVersion): Loadout | undefined {
|
||||
if (gameVersion === "scpc") {
|
||||
gameVersion = "h1"
|
||||
}
|
||||
|
||||
const theLoadouts = this._loadouts[gameVersion] as LoadoutsGameVersion
|
||||
return theLoadouts.loadouts.find((s) => s.id === theLoadouts.selected)
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the loadout data to the Peacock userdata/users folder.
|
||||
*/
|
||||
public async save(): Promise<void> {
|
||||
await writeFile(LOADOUT_PROFILES_FILE, JSON.stringify(this._loadouts))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A synthetic default bind to the global Loadouts instance.
|
||||
*/
|
||||
export const loadouts = new Loadouts()
|
||||
|
||||
/**
|
||||
* Router object for loadout-related web requests.
|
||||
*/
|
||||
export const loadoutRouter = Router()
|
||||
|
||||
if (PEACOCK_DEV) {
|
||||
loadoutRouter.use((_req, res, next) => {
|
||||
res.set("Access-Control-Allow-Origin", "*")
|
||||
res.set(
|
||||
"Access-Control-Allow-Methods",
|
||||
"GET,HEAD,PUT,PATCH,POST,DELETE",
|
||||
)
|
||||
res.set("Access-Control-Allow-Headers", "Content-Type")
|
||||
next()
|
||||
})
|
||||
}
|
||||
|
||||
loadoutRouter.get("/all-loadouts", (req, res) => {
|
||||
res.json(loadouts.loadouts)
|
||||
})
|
||||
|
||||
loadoutRouter.patch("/update", jsonMiddleware(), async (req, res) => {
|
||||
// todo: perform validation on this
|
||||
loadouts.loadouts = req.body
|
||||
|
||||
await loadouts.save()
|
||||
|
||||
res.json({ message: "request completed" })
|
||||
})
|
||||
|
||||
loadoutRouter.patch(
|
||||
"/remove",
|
||||
jsonMiddleware(),
|
||||
async (
|
||||
req: Request<
|
||||
never,
|
||||
string,
|
||||
{ gameVersion: "h1" | "h2" | "h3"; id: string }
|
||||
>,
|
||||
res,
|
||||
) => {
|
||||
// check for gameVersion
|
||||
if (!req.body.gameVersion) {
|
||||
res.status(400).json({ error: "missing gv" })
|
||||
return
|
||||
}
|
||||
|
||||
// validate gameVersion
|
||||
if (!["h1", "h2", "h3"].includes(req.body.gameVersion)) {
|
||||
res.status(400).json({ error: "invalid gv" })
|
||||
return
|
||||
}
|
||||
|
||||
// check for id
|
||||
if (!req.body.id) {
|
||||
res.status(400).json({ error: "missing id" })
|
||||
return
|
||||
}
|
||||
|
||||
const data = loadouts.loadouts
|
||||
|
||||
const withoutDeletionTarget = data[
|
||||
req.body.gameVersion
|
||||
].loadouts.filter((l) => {
|
||||
return l.id !== data[req.body.gameVersion].selected
|
||||
})
|
||||
|
||||
if (withoutDeletionTarget.length === 0) {
|
||||
data[req.body.gameVersion].loadouts = []
|
||||
|
||||
// we have no other loadouts, so make a default one
|
||||
loadouts.createDefault(req.body.gameVersion)
|
||||
} else {
|
||||
// we have other loadouts, so pick the first one
|
||||
data[req.body.gameVersion].loadouts = withoutDeletionTarget
|
||||
data[req.body.gameVersion].selected = withoutDeletionTarget[0]?.id
|
||||
}
|
||||
|
||||
loadouts.loadouts = data
|
||||
await loadouts.save()
|
||||
|
||||
res.json({ message: "request completed" })
|
||||
},
|
||||
)
|
||||
|
||||
loadoutRouter.post("/create", jsonMiddleware(), async (req, res) => {
|
||||
if (!["h1", "h2", "h3"].includes(req.body.gameVersion)) {
|
||||
res.status(400).json({ message: "invalid gv" })
|
||||
return
|
||||
}
|
||||
|
||||
loadouts.createDefault(req.body.gameVersion)
|
||||
await loadouts.save()
|
||||
res.json({ message: "success" })
|
||||
})
|
142
components/loggingInterop.ts
Normal file
142
components/loggingInterop.ts
Normal file
@ -0,0 +1,142 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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 type { NextFunction, Response } from "express"
|
||||
import type { RequestWithJwt } from "./types/types"
|
||||
import picocolors from "picocolors"
|
||||
|
||||
/**
|
||||
* Represents the different log levels.
|
||||
*/
|
||||
export enum LogLevel {
|
||||
/**
|
||||
* For errors. Displays in red.
|
||||
*/
|
||||
ERROR,
|
||||
/**
|
||||
* For warnings. Displays in yellow.
|
||||
*/
|
||||
WARN,
|
||||
/**
|
||||
* For information. Displays in blue.
|
||||
* This is also the fallback for invalid log level values.
|
||||
*/
|
||||
INFO,
|
||||
/**
|
||||
* For debugging.
|
||||
* Displays in light blue, but only if the `DEBUG` environment variable is set to "*", "yes", "true", or "peacock".
|
||||
*/
|
||||
DEBUG,
|
||||
/**
|
||||
* For outputting stacktraces.
|
||||
*/
|
||||
TRACE,
|
||||
}
|
||||
|
||||
const isDebug = ["*", "true", "peacock", "yes"].includes(
|
||||
process.env.DEBUG || "false",
|
||||
)
|
||||
|
||||
/**
|
||||
* Adds leading zeros to a number so that the length of the string will always
|
||||
* be the number of places specified.
|
||||
*
|
||||
* @param num The number.
|
||||
* @param places The intended width of the number (character count).
|
||||
* @example
|
||||
* zeroPad(5, 2) // -> "05"
|
||||
*/
|
||||
const zeroPad = (num: string | number, places: number) =>
|
||||
String(num).padStart(places, "0")
|
||||
|
||||
/**
|
||||
* Outputs a log message to the console.
|
||||
*
|
||||
* @param level The message's level.
|
||||
* @param data The data to output.
|
||||
* @see LogLevel
|
||||
*/
|
||||
export function log(level: LogLevel, data: string): void {
|
||||
const m = data ?? "No message specified"
|
||||
const now = new Date()
|
||||
const stampParts: number[] = [
|
||||
now.getHours(),
|
||||
now.getMinutes(),
|
||||
now.getSeconds(),
|
||||
]
|
||||
const millis = zeroPad(now.getMilliseconds(), 3)
|
||||
const timestamp = `${stampParts
|
||||
.map((part) => zeroPad(part, 2))
|
||||
.join(":")}:${millis}`
|
||||
|
||||
const header = picocolors.gray(timestamp)
|
||||
let outputTransport: (
|
||||
message?: unknown,
|
||||
...optionalParams: unknown[]
|
||||
) => void
|
||||
let levelString: string
|
||||
|
||||
switch (level) {
|
||||
case LogLevel.ERROR:
|
||||
outputTransport = console.error
|
||||
levelString = picocolors.red("Error")
|
||||
break
|
||||
case LogLevel.WARN:
|
||||
outputTransport = console.warn
|
||||
levelString = picocolors.yellow("Warn")
|
||||
break
|
||||
case LogLevel.INFO:
|
||||
default:
|
||||
outputTransport = console.log
|
||||
levelString = picocolors.blue("Info")
|
||||
break
|
||||
case LogLevel.DEBUG:
|
||||
if (!isDebug) {
|
||||
return
|
||||
}
|
||||
outputTransport = console.log
|
||||
levelString = picocolors.blueBright("Debug")
|
||||
break
|
||||
case LogLevel.TRACE:
|
||||
outputTransport = console.trace
|
||||
levelString = picocolors.bgYellow("Trace")
|
||||
break
|
||||
}
|
||||
|
||||
outputTransport(`[${header}] [${levelString}] ${m}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Express middleware that logs all requests and their details with the info log level.
|
||||
*
|
||||
* @param req The Express request object.
|
||||
* @param res The Express response object.
|
||||
* @param next The Express next function.
|
||||
* @see LogLevel.INFO
|
||||
*/
|
||||
export function loggingMiddleware(
|
||||
req: RequestWithJwt,
|
||||
res: Response,
|
||||
next?: NextFunction,
|
||||
): void {
|
||||
log(
|
||||
LogLevel.INFO,
|
||||
`${picocolors.green(req.method)} ${picocolors.underline(req.url)}`,
|
||||
)
|
||||
next?.()
|
||||
}
|
1703
components/menuData.ts
Normal file
1703
components/menuData.ts
Normal file
File diff suppressed because it is too large
Load Diff
452
components/menus/campaigns.ts
Normal file
452
components/menus/campaigns.ts
Normal file
@ -0,0 +1,452 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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 { contractIdToHitObject, controller } from "../controller"
|
||||
import type {
|
||||
Campaign,
|
||||
GameVersion,
|
||||
GenSingleMissionFunc,
|
||||
ICampaignMission,
|
||||
ICampaignVideo,
|
||||
IVideo,
|
||||
StoryData,
|
||||
} from "../types/types"
|
||||
import { log, LogLevel } from "../loggingInterop"
|
||||
import { getConfig } from "../configSwizzleManager"
|
||||
import { fastClone } from "../utils"
|
||||
|
||||
/* eslint-disable prefer-const */
|
||||
|
||||
const genSingleMissionFactory = (userId: string): GenSingleMissionFunc => {
|
||||
return function genSingleMission(
|
||||
contractId: string,
|
||||
gameVersion: GameVersion,
|
||||
): ICampaignMission {
|
||||
const actualContractData = controller.resolveContract(contractId)
|
||||
|
||||
if (!actualContractData) {
|
||||
log(LogLevel.ERROR, `Failed to resolve contract ${contractId}!`)
|
||||
}
|
||||
|
||||
return {
|
||||
Type: "Mission",
|
||||
Data: contractIdToHitObject(contractId, gameVersion, userId),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function genSingleVideo(
|
||||
videoId: string,
|
||||
gameVersion: GameVersion,
|
||||
): ICampaignVideo {
|
||||
const videos = getConfig<Record<string, IVideo>>("Videos", true) // we modify videos so we need to clone this
|
||||
const video = videos[videoId]
|
||||
|
||||
switch (gameVersion) {
|
||||
// H1 is not included here as there should be no edits required for the videos from H1
|
||||
case "h2": {
|
||||
if (video.Data.DlcName === "GAME_STORE_METADATA_GAME_TITLE") {
|
||||
video.Data.DlcName = "GAME_STORE_METADATA_S2_GAME_TITLE"
|
||||
video.Data.DlcImage =
|
||||
"images/livetile/dlc/wide_logo_hitman2.png"
|
||||
}
|
||||
|
||||
if (video.Data.DlcName.includes("METADATA_DLC")) {
|
||||
video.Data.DlcName.replace(
|
||||
"METADATA_DLC",
|
||||
"METADATA_LEGACY_DLC",
|
||||
)
|
||||
video.Data.DlcImage.replace("wide_logo", "wide_logo_legacy")
|
||||
}
|
||||
|
||||
// Void entitlements unless it's LOCATION_NEWZEALAND, that is currently the only known entitlement for S2 maps
|
||||
if (!video.Entitlements.includes("LOCATION_NEWZEALAND")) {
|
||||
video.Entitlements = []
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "h3": {
|
||||
video.Data = {
|
||||
DlcName: "GAME_STORE_METADATA_S3_GAME_TITLE",
|
||||
DlcImage: "images/livetile/dlc/tile_hitman3.jpg",
|
||||
}
|
||||
|
||||
if (video.Entitlements.includes("GOTY_PATIENT_ZERO")) {
|
||||
video.Entitlements = ["H1_LEGACY_STANDARD"]
|
||||
}
|
||||
|
||||
if (video.Entitlements.includes("LOCATION_NEWZEALAND")) {
|
||||
video.Entitlements = ["H2_LEGACY_STANDARD"]
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
return {
|
||||
Type: "Video",
|
||||
Data: video,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the campaigns data fed to the game's hub route.
|
||||
*
|
||||
* @param gameVersion The game's version.
|
||||
* @param userId The current user's ID.
|
||||
* @returns The campaigns.
|
||||
*/
|
||||
export function makeCampaigns(
|
||||
gameVersion: GameVersion,
|
||||
userId: string,
|
||||
): Campaign[] {
|
||||
const genSingleMission = genSingleMissionFactory(userId)
|
||||
|
||||
let c: Campaign[] = []
|
||||
|
||||
const prologueStoryData: StoryData[] = [
|
||||
genSingleMission("1436cbe4-164b-450f-ad2c-77dec88f53dd", gameVersion),
|
||||
genSingleVideo("prologue_intermission1", gameVersion),
|
||||
genSingleMission("1d241b00-f585-4e3d-bc61-3095af1b96e2", gameVersion),
|
||||
genSingleVideo("prologue_intermission2", gameVersion),
|
||||
genSingleMission("b573932d-7a34-44f1-bcf4-ea8f79f75710", gameVersion),
|
||||
genSingleVideo("prologue_intermission3", gameVersion),
|
||||
genSingleMission("ada5f2b1-8529-48bb-a596-717f75f5eacb", gameVersion),
|
||||
genSingleVideo("prologue_intermission4", gameVersion),
|
||||
]
|
||||
|
||||
const s1StoryData: StoryData[] = [
|
||||
genSingleMission("00000000-0000-0000-0000-000000000200", gameVersion),
|
||||
genSingleVideo("debriefing_peacock", gameVersion),
|
||||
genSingleMission("00000000-0000-0000-0000-000000000600", gameVersion),
|
||||
genSingleVideo("debriefing_octopus", gameVersion),
|
||||
genSingleMission("00000000-0000-0000-0000-000000000400", gameVersion),
|
||||
genSingleVideo("debriefing_spider", gameVersion),
|
||||
genSingleMission("db341d9f-58a4-411d-be57-0bc4ed85646b", gameVersion),
|
||||
genSingleVideo("debriefing_tiger", gameVersion),
|
||||
genSingleMission("42bac555-bbb9-429d-a8ce-f1ffdf94211c", gameVersion),
|
||||
genSingleVideo("bull_secret_room", gameVersion),
|
||||
genSingleVideo("debriefing_bull", gameVersion),
|
||||
genSingleMission("0e81a82e-b409-41e9-9e3b-5f82e57f7a12", gameVersion),
|
||||
genSingleVideo("debriefing_snowcrane", gameVersion),
|
||||
]
|
||||
|
||||
let prologueCampaign: Campaign,
|
||||
s1Campaign: Campaign,
|
||||
s2Campaign: Campaign,
|
||||
s3Campaign: Campaign | undefined
|
||||
|
||||
if (gameVersion !== "h1") {
|
||||
const s2StoryData: StoryData[] = [
|
||||
genSingleMission(
|
||||
"c65019e5-43a8-4a33-8a2a-84c750a5eeb3",
|
||||
gameVersion,
|
||||
),
|
||||
genSingleVideo("debriefing_sheep", gameVersion),
|
||||
genSingleMission(
|
||||
"c1d015b4-be08-4e44-808e-ada0f387656f",
|
||||
gameVersion,
|
||||
),
|
||||
genSingleVideo("debriefing_flamingo", gameVersion),
|
||||
genSingleMission(
|
||||
"422519be-ed2e-44df-9dac-18f739d44fd9",
|
||||
gameVersion,
|
||||
),
|
||||
genSingleVideo("debriefing_hippo", gameVersion),
|
||||
genSingleMission(
|
||||
"0fad48d7-3d0f-4c66-8605-6cbe9c3a46d7",
|
||||
gameVersion,
|
||||
),
|
||||
genSingleVideo("debriefing_mongoose", gameVersion),
|
||||
genSingleVideo("intro_skunk", gameVersion),
|
||||
genSingleMission(
|
||||
"82f55837-e26c-41bf-bc6e-fa97b7981fbc",
|
||||
gameVersion,
|
||||
),
|
||||
genSingleVideo("debriefing_skunk", gameVersion),
|
||||
genSingleVideo("intro_magpie", gameVersion),
|
||||
genSingleMission(
|
||||
"0d225edf-40cd-4f20-a30f-b62a373801d3",
|
||||
gameVersion,
|
||||
),
|
||||
genSingleVideo("debriefing_magpie", gameVersion),
|
||||
genSingleMission(
|
||||
"7a03a97d-238c-48bd-bda0-e5f279569cce",
|
||||
gameVersion,
|
||||
),
|
||||
genSingleMission(
|
||||
"095261b5-e15b-4ca1-9bb7-001fb85c5aaa",
|
||||
gameVersion,
|
||||
),
|
||||
]
|
||||
|
||||
const s3StoryData: StoryData[] | undefined =
|
||||
gameVersion === "h3"
|
||||
? [
|
||||
genSingleVideo("intro_gecko", gameVersion),
|
||||
genSingleMission(
|
||||
"7d85f2b0-80ca-49be-a2b7-d56f67faf252",
|
||||
gameVersion,
|
||||
),
|
||||
genSingleVideo("debriefing_gecko", gameVersion),
|
||||
genSingleMission(
|
||||
"755984a8-fb0b-4673-8637-95cfe7d34e0f",
|
||||
gameVersion,
|
||||
),
|
||||
genSingleVideo("debriefing_bulldog", gameVersion),
|
||||
genSingleMission(
|
||||
"ebcd14b2-0786-4ceb-a2a4-e771f60d0125",
|
||||
gameVersion,
|
||||
),
|
||||
genSingleVideo("debriefing_fox", gameVersion),
|
||||
genSingleMission(
|
||||
"3d0cbb8c-2a80-442a-896b-fea00e98768c",
|
||||
gameVersion,
|
||||
),
|
||||
genSingleVideo("debriefing_rat", gameVersion),
|
||||
genSingleMission(
|
||||
"d42f850f-ca55-4fc9-9766-8c6a2b5c3129",
|
||||
gameVersion,
|
||||
),
|
||||
genSingleVideo("debriefing_llama", gameVersion),
|
||||
genSingleMission(
|
||||
"a3e19d55-64a6-4282-bb3c-d18c3f3e6e29",
|
||||
gameVersion,
|
||||
),
|
||||
genSingleVideo("debriefing_wolverine", gameVersion),
|
||||
]
|
||||
: undefined
|
||||
|
||||
// BackgroundImage is duplicated as H3 uses properties, H2 doesn't
|
||||
prologueCampaign = {
|
||||
BackgroundImage: "images/story/background_training.jpg",
|
||||
Image: "",
|
||||
Name: "UI_CAMPAIGN_ICA_FACILITY_TITLE",
|
||||
Properties: {
|
||||
BackgroundImage: "images/story/background_training.jpg",
|
||||
},
|
||||
StoryData: prologueStoryData,
|
||||
Type: "training",
|
||||
}
|
||||
|
||||
s1Campaign = {
|
||||
BackgroundImage: "images/story/background_season1.jpg",
|
||||
Image: "",
|
||||
Name: "UI_SEASON_1",
|
||||
Properties: {
|
||||
BackgroundImage: "images/story/background_season1.jpg",
|
||||
},
|
||||
StoryData: s1StoryData,
|
||||
Type: "mission",
|
||||
}
|
||||
|
||||
s2Campaign = {
|
||||
BackgroundImage: "images/story/background_season2.jpg",
|
||||
Image: "",
|
||||
Name: "UI_SEASON_2",
|
||||
Properties: {
|
||||
BackgroundImage: "images/story/background_season2.jpg",
|
||||
},
|
||||
StoryData: s2StoryData,
|
||||
Type: "mission",
|
||||
}
|
||||
|
||||
s3Campaign =
|
||||
gameVersion === "h3"
|
||||
? {
|
||||
BackgroundImage: null,
|
||||
Image: "",
|
||||
Name: "UI_SEASON_3",
|
||||
Properties: {
|
||||
BackgroundImage:
|
||||
"images/story/background_season3.jpg",
|
||||
},
|
||||
StoryData: s3StoryData!,
|
||||
Type: "mission",
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
|
||||
const pzCampaign: Campaign = {
|
||||
Name: "UI_CONTRACT_CAMPAIGN_WHITE_SPIDER_TITLE",
|
||||
Image: "images/story/tile_whitespider.jpg",
|
||||
Type: "campaign",
|
||||
BackgroundImage:
|
||||
gameVersion === "h1"
|
||||
? null
|
||||
: "images/story/background_whitespider.jpg",
|
||||
StoryData: [
|
||||
genSingleMission(
|
||||
"024b6964-a3bb-4457-b085-08f9a7dc7fb7",
|
||||
gameVersion,
|
||||
),
|
||||
genSingleMission(
|
||||
"7e3f758a-2435-42de-93bd-d8f0b72c63a4",
|
||||
gameVersion,
|
||||
),
|
||||
genSingleMission(
|
||||
"ada6205e-6ee8-4189-9cdb-4947cccd84f4",
|
||||
gameVersion,
|
||||
),
|
||||
genSingleMission(
|
||||
"a2befcec-7799-4987-9215-6a152cb6a320",
|
||||
gameVersion,
|
||||
),
|
||||
],
|
||||
}
|
||||
|
||||
switch (gameVersion) {
|
||||
case "h1": {
|
||||
c.push(
|
||||
{
|
||||
Name: "UI_SEASON_1",
|
||||
Image: "images/story/tile_season1.jpg",
|
||||
Type: "mission",
|
||||
BackgroundImage: null,
|
||||
StoryData: prologueStoryData.concat(s1StoryData),
|
||||
},
|
||||
pzCampaign,
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
case "h2": {
|
||||
c.push(prologueCampaign!, s1Campaign!, s2Campaign!, pzCampaign)
|
||||
break
|
||||
}
|
||||
|
||||
case "h3": {
|
||||
c.push(prologueCampaign!, s1Campaign!, s2Campaign!, s3Campaign!, {
|
||||
Name: "UI_MENU_PAGE_SIDE_MISSIONS_TITLE",
|
||||
Image: "",
|
||||
Type: "mission",
|
||||
BackgroundImage: null,
|
||||
Subgroups: [
|
||||
pzCampaign,
|
||||
{
|
||||
Name: "UI_MENU_PAGE_BONUS_MISSIONS_TITLE",
|
||||
Image: "",
|
||||
Type: "campaign",
|
||||
BackgroundImage:
|
||||
"images/story/background_bonus_missions.jpg",
|
||||
StoryData: [
|
||||
genSingleMission(
|
||||
"00000000-0000-0000-0001-000000000006",
|
||||
gameVersion,
|
||||
),
|
||||
genSingleMission(
|
||||
"00000000-0000-0000-0001-000000000005",
|
||||
gameVersion,
|
||||
),
|
||||
genSingleMission(
|
||||
"ced93d8f-9535-425a-beb9-ef219e781e81",
|
||||
gameVersion,
|
||||
),
|
||||
genSingleMission(
|
||||
"c414a084-a7b9-43ce-b6ca-590620acd87e",
|
||||
gameVersion,
|
||||
),
|
||||
genSingleMission(
|
||||
"4e45e91a-94ca-4d89-89fc-1b250e608e73",
|
||||
gameVersion,
|
||||
),
|
||||
genSingleMission(
|
||||
"99bd3287-1d83-4429-a769-45045dfcbf31",
|
||||
gameVersion,
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
Name: "UI_MENU_PAGE_SPECIAL_ASSIGNMENTS_TITLE",
|
||||
Image: "",
|
||||
Type: "campaign",
|
||||
BackgroundImage:
|
||||
"images/story/background_special_assignments.jpg",
|
||||
StoryData: [
|
||||
genSingleMission(
|
||||
"179563a4-727a-4072-b354-c9fff4e8bff0",
|
||||
gameVersion,
|
||||
),
|
||||
genSingleMission(
|
||||
"a8036782-de0a-4353-b522-0ab7a384bade",
|
||||
gameVersion,
|
||||
),
|
||||
genSingleMission(
|
||||
"f1ba328f-e3dd-4ef8-bb26-0363499fdd95",
|
||||
gameVersion,
|
||||
),
|
||||
genSingleMission(
|
||||
"0b616e62-af0c-495b-82e3-b778e82b5912",
|
||||
gameVersion,
|
||||
),
|
||||
],
|
||||
},
|
||||
].filter((o) => o !== undefined),
|
||||
StoryData: [],
|
||||
Properties: {
|
||||
BackgroundImage:
|
||||
"images/story/background_side_missions.jpg",
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
controller.hooks.contributeCampaigns.call(
|
||||
c,
|
||||
genSingleMission,
|
||||
genSingleVideo,
|
||||
gameVersion,
|
||||
)
|
||||
|
||||
return c.filter(Boolean)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all campaigns (including sub-campaigns) for the specified game version.
|
||||
*
|
||||
* @param gameVersion The game version.
|
||||
* @param userId The user's ID.
|
||||
*/
|
||||
export function getAllCampaigns(
|
||||
gameVersion: GameVersion,
|
||||
userId: string,
|
||||
): Campaign[] {
|
||||
// Warning: cloning this is required, as pushing to this field would normally
|
||||
// modify the actual campaigns object.
|
||||
const list = fastClone(makeCampaigns(gameVersion, userId))
|
||||
|
||||
const deepIterateCampaigns = (current: Campaign[]) => {
|
||||
for (const c of current) {
|
||||
if (c.Subgroups) {
|
||||
list.push(...c.Subgroups)
|
||||
deepIterateCampaigns(c.Subgroups)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deepIterateCampaigns(list)
|
||||
|
||||
return list
|
||||
}
|
151
components/menus/destinations.ts
Normal file
151
components/menus/destinations.ts
Normal file
@ -0,0 +1,151 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 The Peacock Project Team
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { getVersionedConfig } from "../configSwizzleManager"
|
||||
import type {
|
||||
CompletionData,
|
||||
DestinationsMenuDataObject,
|
||||
GameLocationsData,
|
||||
GameVersion,
|
||||
PeacockLocationsData,
|
||||
RequestWithJwt,
|
||||
Unlockable,
|
||||
} from "../types/types"
|
||||
import { controller } from "../controller"
|
||||
import { generateCompletionData } from "../contracts/dataGen"
|
||||
|
||||
type GameFacingDestination = {
|
||||
ChallengeCompletion: {
|
||||
ChallengesCount: number
|
||||
CompletedChallengesCount: number
|
||||
}
|
||||
CompletionData: CompletionData
|
||||
OpportunityStatistics: {
|
||||
Count: number
|
||||
Completed: number
|
||||
}
|
||||
LocationCompletionPercent: number
|
||||
Location: Unlockable
|
||||
}
|
||||
|
||||
export function destinationsMenu(req: RequestWithJwt): GameFacingDestination[] {
|
||||
const destinations = getVersionedConfig<DestinationsMenuDataObject[]>(
|
||||
"Destinations",
|
||||
req.gameVersion,
|
||||
false,
|
||||
)
|
||||
|
||||
const result: GameFacingDestination[] = []
|
||||
|
||||
const locations = getVersionedConfig<PeacockLocationsData>(
|
||||
"LocationsData",
|
||||
req.gameVersion,
|
||||
true,
|
||||
)
|
||||
|
||||
for (const destination of destinations) {
|
||||
const parent = locations.parents[destination.ParentId]
|
||||
|
||||
parent.GameAsset = null
|
||||
parent.DisplayNameLocKey =
|
||||
"UI_LOCATION_PARENT_" + destination.ParentId.substring(16) + "_NAME"
|
||||
|
||||
const template = {
|
||||
ChallengeCompletion: {
|
||||
ChallengesCount: 0,
|
||||
CompletedChallengesCount: 0, // TODO: Hook this up to challenge counts.
|
||||
},
|
||||
CompletionData: generateCompletionData(
|
||||
destination.ParentId,
|
||||
req.jwt.unique_name,
|
||||
req.gameVersion,
|
||||
),
|
||||
OpportunityStatistics: {
|
||||
Count: 0,
|
||||
Completed: 0,
|
||||
},
|
||||
LocationCompletionPercent: 0,
|
||||
Location: parent,
|
||||
}
|
||||
|
||||
result.push(template)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the game's LocationsData object, and optionally removes locations
|
||||
* that don't provide a contract creation ID.
|
||||
*
|
||||
* @param gameVersion The game version.
|
||||
* @param excludeIfNoContracts If true, locations that don't support contract
|
||||
* creation will not be returned.
|
||||
* @returns The locations that can be played.
|
||||
*/
|
||||
export function createLocationsData(
|
||||
gameVersion: GameVersion,
|
||||
excludeIfNoContracts = false,
|
||||
): GameLocationsData {
|
||||
const locData = getVersionedConfig<PeacockLocationsData>(
|
||||
"LocationsData",
|
||||
gameVersion,
|
||||
true,
|
||||
)
|
||||
|
||||
const allSublocationIds = Object.keys(locData.children)
|
||||
|
||||
const finalData: GameLocationsData = {
|
||||
Data: {
|
||||
HasMore: false,
|
||||
Page: 0,
|
||||
Locations: [],
|
||||
},
|
||||
}
|
||||
|
||||
for (const sublocationId of allSublocationIds) {
|
||||
const sublocation = locData.children[sublocationId]
|
||||
const parentLocation =
|
||||
locData.parents[sublocation.Properties.ParentLocation]
|
||||
const creationContract = controller.resolveContract(
|
||||
sublocation.Properties.CreateContractId,
|
||||
)
|
||||
|
||||
if (!creationContract && excludeIfNoContracts) {
|
||||
continue
|
||||
}
|
||||
|
||||
const toAdd = {
|
||||
Location: parentLocation,
|
||||
SubLocation: sublocation,
|
||||
Contract: {
|
||||
Metadata: {},
|
||||
Data: {},
|
||||
},
|
||||
}
|
||||
|
||||
if (creationContract) {
|
||||
toAdd.Contract = creationContract
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
finalData.Data.Locations.push(toAdd as any)
|
||||
}
|
||||
|
||||
return finalData
|
||||
}
|
149
components/menus/favoriteContracts.ts
Normal file
149
components/menus/favoriteContracts.ts
Normal file
@ -0,0 +1,149 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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 type {
|
||||
GameVersion,
|
||||
MissionManifest,
|
||||
RequestWithJwt,
|
||||
Unlockable,
|
||||
UserCentricContract,
|
||||
} from "../types/types"
|
||||
import { controller } from "../controller"
|
||||
import { generateUserCentric } from "../contracts/dataGen"
|
||||
import { getUserData, writeUserData } from "../databaseHandler"
|
||||
import { getConfig, getVersionedConfig } from "../configSwizzleManager"
|
||||
import type { Response } from "express"
|
||||
|
||||
export function withLookupDialog(
|
||||
req: RequestWithJwt<{ contractId: string }>,
|
||||
res: Response,
|
||||
): void {
|
||||
const lookupFavoriteTemplate = getConfig(
|
||||
"LookupContractFavoriteTemplate",
|
||||
false,
|
||||
)
|
||||
|
||||
if (!req.query.contractId) {
|
||||
res.status(400).send("no contract id")
|
||||
return
|
||||
}
|
||||
|
||||
const contract = controller.resolveContract(req.query.contractId)
|
||||
|
||||
if (!contract) {
|
||||
res.status(404).send("contract does not exist!")
|
||||
return
|
||||
}
|
||||
|
||||
interface Result {
|
||||
template: unknown
|
||||
data: {
|
||||
Contract: MissionManifest
|
||||
UserCentricContract: UserCentricContract
|
||||
Location: unknown
|
||||
AddedSuccessfully?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
const sublocation = getVersionedConfig<readonly Unlockable[]>(
|
||||
"allunlockables",
|
||||
req.gameVersion,
|
||||
false,
|
||||
).find((entry) => entry.Id === contract.Metadata.Location)
|
||||
|
||||
const result: Result = {
|
||||
template: lookupFavoriteTemplate,
|
||||
data: {
|
||||
Contract: contract,
|
||||
Location: sublocation,
|
||||
UserCentricContract: generateUserCentric(
|
||||
contract,
|
||||
req.jwt.unique_name,
|
||||
req.gameVersion,
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
result.data.AddedSuccessfully = toggleFavorite(
|
||||
req.jwt.unique_name,
|
||||
req.query.contractId,
|
||||
req.gameVersion,
|
||||
)
|
||||
|
||||
res.json(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles a contract as a user's favorite.
|
||||
*
|
||||
* @param userId The user's ID.
|
||||
* @param contractId The contract's ID.
|
||||
* @param gameVersion The game's version.
|
||||
* @returns If the contract is a favorite after the operation or not.
|
||||
*/
|
||||
export function toggleFavorite(
|
||||
userId: string,
|
||||
contractId: string,
|
||||
gameVersion: GameVersion,
|
||||
): boolean {
|
||||
let flag = false
|
||||
const userProfile = getUserData(userId, gameVersion)
|
||||
|
||||
if (!Array.isArray(userProfile.Extensions.PeacockFavoriteContracts)) {
|
||||
userProfile.Extensions.PeacockFavoriteContracts = []
|
||||
}
|
||||
|
||||
if (userProfile.Extensions.PeacockFavoriteContracts.includes(contractId)) {
|
||||
userProfile.Extensions.PeacockFavoriteContracts =
|
||||
userProfile.Extensions.PeacockFavoriteContracts.filter(
|
||||
(f) => f !== contractId,
|
||||
)
|
||||
} else {
|
||||
userProfile.Extensions.PeacockFavoriteContracts.push(contractId)
|
||||
flag = true
|
||||
}
|
||||
|
||||
writeUserData(userId, gameVersion)
|
||||
return flag
|
||||
}
|
||||
|
||||
export function directRoute(req: RequestWithJwt, res: Response): void {
|
||||
if (!req.params.contractId) {
|
||||
res.status(400).send("no contract id")
|
||||
return
|
||||
}
|
||||
|
||||
const contract = controller.resolveContract(req.params.contractId)
|
||||
|
||||
if (!contract) {
|
||||
res.status(404).send("contract does not exist!")
|
||||
return
|
||||
}
|
||||
|
||||
res.json({
|
||||
template: null,
|
||||
data: {
|
||||
ContractId: req.params.contractId,
|
||||
IsInPlaylist: toggleFavorite(
|
||||
req.jwt.unique_name,
|
||||
req.params.contractId,
|
||||
req.gameVersion,
|
||||
),
|
||||
},
|
||||
})
|
||||
}
|
104
components/menus/imageHandler.ts
Normal file
104
components/menus/imageHandler.ts
Normal file
@ -0,0 +1,104 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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 type { RequestWithJwt } from "../types/types"
|
||||
import type { Response } from "express"
|
||||
import parseUrl from "parseurl"
|
||||
import axios from "axios"
|
||||
import { log, LogLevel } from "../loggingInterop"
|
||||
import { getFlag } from "../flags"
|
||||
import { createWriteStream } from "fs"
|
||||
|
||||
const dangerousBidiChars = [
|
||||
"\u061C",
|
||||
"\u200E",
|
||||
"\u200F",
|
||||
"\u202A",
|
||||
"\u202B",
|
||||
"\u202C",
|
||||
"\u202D",
|
||||
"\u202E",
|
||||
"\u2066",
|
||||
"\u2067",
|
||||
"\u2068",
|
||||
"\u2069",
|
||||
]
|
||||
|
||||
export async function imageFetchingMiddleware(
|
||||
req: RequestWithJwt,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
if (getFlag("imageLoading") === "OFFLINE") {
|
||||
res.status(404).send("Image not offline, unable to provide.")
|
||||
return
|
||||
}
|
||||
|
||||
const originalUrl = parseUrl.original(req)
|
||||
const path = parseUrl(req)?.pathname
|
||||
|
||||
if (!path) {
|
||||
return
|
||||
}
|
||||
|
||||
// make sure redirect occurs at mount
|
||||
if (path === "/" && originalUrl?.pathname?.slice(-1) !== "/") {
|
||||
res.status(404).send("Not found!")
|
||||
return
|
||||
}
|
||||
|
||||
if (path.includes("./")) {
|
||||
res.status(400).send("Hey, you can't write/read arbitrary files!!")
|
||||
return
|
||||
}
|
||||
|
||||
for (const char of dangerousBidiChars) {
|
||||
if (path.includes(char)) {
|
||||
res.status(400).send(
|
||||
"Hey, you can't put malicious bidi characters in the URL!!",
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const axiosResponse = await axios(
|
||||
`https://img.rdil.rocks/images${path}`,
|
||||
{
|
||||
responseType: "stream",
|
||||
},
|
||||
)
|
||||
|
||||
if (path.endsWith(".jpg")) {
|
||||
res.header("Content-Type", "image/jpg")
|
||||
} else if (path.endsWith(".png")) {
|
||||
res.header("Content-Type", "image/png")
|
||||
}
|
||||
|
||||
axiosResponse.data.pipe(res)
|
||||
|
||||
if (getFlag("imageLoading") === "SAVEONREQUESTED") {
|
||||
// we got the image, we should be fine
|
||||
// may need to introduce extra security here in the future, not sure though
|
||||
// we've got bidi and escape paths taken care of, so it should be enough, I hope?
|
||||
axiosResponse.data.pipe(createWriteStream(`images${path}`))
|
||||
}
|
||||
} catch (e) {
|
||||
log(LogLevel.DEBUG, `Err ${e} ${e.stack}`)
|
||||
res.status(500).send("failed to get data")
|
||||
}
|
||||
}
|
256
components/menus/menuSystem.ts
Normal file
256
components/menus/menuSystem.ts
Normal file
@ -0,0 +1,256 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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 send from "send"
|
||||
import { imageFetchingMiddleware } from "./imageHandler"
|
||||
import { SyncBailHook, SyncHook } from "../hooksImpl"
|
||||
|
||||
const menuSystemRouter = Router()
|
||||
|
||||
// /resources-8-10/
|
||||
|
||||
/**
|
||||
* 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",
|
||||
)
|
||||
}
|
||||
|
||||
if (gameVersion === "h3" || gameVersion === "h1") {
|
||||
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",
|
||||
)
|
||||
}
|
||||
|
||||
if (gameVersion === "h2") {
|
||||
configs.push("menusystem/data/ismultiplayeravailable.json")
|
||||
configs.push(
|
||||
"menusystem/pages/multiplayer/content/lobbyslim.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 "/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)
|
||||
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)
|
||||
|
||||
menuSystemRouter.use(
|
||||
"/images/",
|
||||
serveStatic("images", { fallthrough: true }),
|
||||
imageFetchingMiddleware,
|
||||
)
|
||||
|
||||
// Miranda Jamison's image path in the repository is escaped for some reason
|
||||
menuSystemRouter.get(
|
||||
"/images%5Cactors%5Celusive_goldendoublet_face.jpg",
|
||||
(req, res) => {
|
||||
send(req, "images/actors/elusive_goldendoublet_face.jpg").pipe(res)
|
||||
},
|
||||
)
|
||||
|
||||
// Sully Bowden is the same (come on IOI!)
|
||||
menuSystemRouter.get(
|
||||
"/images%5Cactors%5Celusive_redsnapper_face.jpg",
|
||||
(req, res) => {
|
||||
send(req, "images/actors/elusive_redsnapper_face.jpg").pipe(res)
|
||||
},
|
||||
)
|
||||
|
||||
export { menuSystemRouter }
|
443
components/menus/planning.ts
Normal file
443
components/menus/planning.ts
Normal file
@ -0,0 +1,443 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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 type { RequestWithJwt, SceneConfig } from "../types/types"
|
||||
import { log, LogLevel } from "../loggingInterop"
|
||||
import { _legacyBull, _theLastYardbirdScpc, controller } from "../controller"
|
||||
import {
|
||||
contractIdToEscalationGroupId,
|
||||
getLevelCount,
|
||||
getUserEscalationProgress,
|
||||
resetUserEscalationProgress,
|
||||
} from "../contracts/escalations/escalationService"
|
||||
import {
|
||||
generateUserCentric,
|
||||
getSubLocationFromContract,
|
||||
mapObjectives,
|
||||
} from "../contracts/dataGen"
|
||||
import { getConfig } from "../configSwizzleManager"
|
||||
import { getUserData, writeUserData } from "../databaseHandler"
|
||||
import { nilUuid, unlockorderComparer } from "../utils"
|
||||
|
||||
import type { Response } from "express"
|
||||
import { createInventory } from "../inventory"
|
||||
import { createSniperLoadouts } from "./sniper"
|
||||
import { getFlag } from "../flags"
|
||||
import { loadouts } from "../loadouts"
|
||||
import { resolveProfiles } from "../profileHandler"
|
||||
import { PlanningQuery } from "../types/gameSchemas"
|
||||
|
||||
export async function planningView(
|
||||
req: RequestWithJwt<PlanningQuery>,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
if (!req.query.contractid || !req.query.resetescalation) {
|
||||
res.status(400).send("invalid query")
|
||||
return
|
||||
}
|
||||
|
||||
const entranceData = getConfig<SceneConfig>("Entrances", false)
|
||||
const missionStories = getConfig<Record<string, unknown>>(
|
||||
"MissionStories",
|
||||
false,
|
||||
)
|
||||
|
||||
const userData = getUserData(req.jwt.unique_name, req.gameVersion)
|
||||
|
||||
const isForReset = req.query.resetescalation === "true"
|
||||
|
||||
if (isForReset) {
|
||||
const escalationGroupId = contractIdToEscalationGroupId(
|
||||
req.query.contractid,
|
||||
)
|
||||
|
||||
resetUserEscalationProgress(userData, escalationGroupId)
|
||||
|
||||
writeUserData(req.jwt.unique_name, req.gameVersion)
|
||||
|
||||
// now reassign properties and continue
|
||||
req.query.contractid =
|
||||
controller.escalationMappings[escalationGroupId]["1"]
|
||||
}
|
||||
|
||||
const contractData =
|
||||
req.gameVersion === "h1" &&
|
||||
req.query.contractid === "42bac555-bbb9-429d-a8ce-f1ffdf94211c"
|
||||
? _legacyBull
|
||||
: req.query.contractid === "ff9f46cf-00bd-4c12-b887-eac491c3a96d"
|
||||
? _theLastYardbirdScpc
|
||||
: controller.resolveContract(req.query.contractid)
|
||||
|
||||
if (!contractData) {
|
||||
log(LogLevel.ERROR, `Not found: ${req.query.contractid}.`)
|
||||
res.status(400).send("no ct")
|
||||
return
|
||||
}
|
||||
|
||||
const groupData = {
|
||||
GroupId: undefined as string | undefined,
|
||||
GroupTitle: undefined as string | undefined,
|
||||
CompletedLevels: undefined as number | undefined,
|
||||
Completed: undefined as boolean | undefined,
|
||||
TotalLevels: undefined as number | undefined,
|
||||
BestScore: undefined as number | undefined,
|
||||
BestPlayer: undefined as string | undefined,
|
||||
BestLevel: undefined as number | undefined,
|
||||
}
|
||||
|
||||
const escalationGroupId = contractIdToEscalationGroupId(
|
||||
req.query.contractid,
|
||||
)
|
||||
|
||||
if (escalationGroupId) {
|
||||
const p = getUserEscalationProgress(userData, escalationGroupId)
|
||||
const done =
|
||||
userData.Extensions.PeacockCompletedEscalations.includes(
|
||||
escalationGroupId,
|
||||
)
|
||||
|
||||
groupData.GroupId = escalationGroupId
|
||||
groupData.GroupTitle = contractData.Metadata.Title
|
||||
groupData.CompletedLevels = done ? p : p - 1
|
||||
groupData.Completed = done
|
||||
groupData.TotalLevels = getLevelCount(
|
||||
controller.escalationMappings[escalationGroupId],
|
||||
)
|
||||
groupData.BestScore = 0
|
||||
groupData.BestPlayer = nilUuid
|
||||
groupData.BestLevel = 0
|
||||
}
|
||||
|
||||
if (!contractData) {
|
||||
log(LogLevel.WARN, `Unknown contract: ${req.query.contractid}`)
|
||||
res.status(404).send("contract not found!")
|
||||
return
|
||||
}
|
||||
|
||||
const creatorProfile = (
|
||||
await resolveProfiles(
|
||||
[
|
||||
contractData.Metadata.CreatorUserId || "",
|
||||
"fadb923c-e6bb-4283-a537-eb4d1150262e",
|
||||
],
|
||||
req.gameVersion,
|
||||
)
|
||||
)[0]
|
||||
|
||||
const scenePath = contractData.Metadata.ScenePath.toLowerCase()
|
||||
|
||||
log(
|
||||
LogLevel.DEBUG,
|
||||
`Looking up details for contract - Location:${contractData.Metadata.Location} (${scenePath})`,
|
||||
)
|
||||
|
||||
const sublocation = getSubLocationFromContract(
|
||||
contractData,
|
||||
req.gameVersion,
|
||||
)
|
||||
|
||||
if (!Object.prototype.hasOwnProperty.call(entranceData, scenePath)) {
|
||||
log(
|
||||
LogLevel.ERROR,
|
||||
`Could not find Entrance data for ${scenePath} (loc Planning)! This may cause an unhandled promise rejection.`,
|
||||
)
|
||||
}
|
||||
|
||||
const entrancesInScene = entranceData[scenePath]
|
||||
|
||||
const typedInv = createInventory(
|
||||
req.jwt.unique_name,
|
||||
req.gameVersion,
|
||||
userData.Extensions.entP,
|
||||
)
|
||||
|
||||
const unlockedEntrances = typedInv
|
||||
.filter((item) => item.Unlockable.Type === "access")
|
||||
.map((i) => i.Unlockable)
|
||||
.filter((unlockable) => unlockable.Properties.RepositoryId)
|
||||
|
||||
if (!unlockedEntrances) {
|
||||
log(
|
||||
LogLevel.ERROR,
|
||||
"No matching entrance data found in planning, this is a bug!",
|
||||
)
|
||||
}
|
||||
|
||||
sublocation.DisplayNameLocKey = `UI_${sublocation.Id}_NAME`
|
||||
|
||||
// Default loadout
|
||||
|
||||
let currentLoadout = loadouts.getLoadoutFor(req.gameVersion)
|
||||
|
||||
if (!currentLoadout) {
|
||||
currentLoadout = loadouts.createDefault(req.gameVersion)
|
||||
}
|
||||
|
||||
let pistol = "FIREARMS_HERO_PISTOL_TACTICAL_ICA_19"
|
||||
let suit = "TOKEN_OUTFIT_HITMANSUIT"
|
||||
let tool1 = "TOKEN_FIBERWIRE"
|
||||
let tool2 = "PROP_TOOL_COIN"
|
||||
let briefcaseProp: string | undefined = undefined
|
||||
let briefcaseId: string | undefined = undefined
|
||||
|
||||
const hasOwn = Object.prototype.hasOwnProperty.bind(currentLoadout.data)
|
||||
|
||||
const dlForLocation =
|
||||
getFlag("loadoutSaving") === "LEGACY"
|
||||
? // older default loadout setting (per-person)
|
||||
userData.Extensions.defaultloadout?.[
|
||||
contractData.Metadata.Location
|
||||
]
|
||||
: // new loadout profiles system
|
||||
hasOwn(contractData.Metadata.Location) &&
|
||||
currentLoadout.data[contractData.Metadata.Location]
|
||||
|
||||
if (dlForLocation) {
|
||||
pistol = dlForLocation["2"]
|
||||
suit = dlForLocation["3"]
|
||||
tool1 = dlForLocation["4"]
|
||||
tool2 = dlForLocation["5"]
|
||||
for (const key of Object.keys(dlForLocation)) {
|
||||
if (["2", "3", "4", "5"].includes(key)) {
|
||||
// we're looking for keys that aren't taken up by other things
|
||||
continue
|
||||
}
|
||||
|
||||
briefcaseId = key
|
||||
briefcaseProp = dlForLocation[key]
|
||||
}
|
||||
}
|
||||
|
||||
const i = typedInv.find((item) => item.Unlockable.Id === briefcaseProp)
|
||||
|
||||
const escalation = contractData.Metadata.Type === "escalation"
|
||||
|
||||
const userCentric = generateUserCentric(
|
||||
contractData,
|
||||
req.jwt.unique_name,
|
||||
req.gameVersion,
|
||||
)
|
||||
|
||||
if (userCentric.Contract.Metadata.Type === "elusive") {
|
||||
// change the type until we figure out why they become unplayable
|
||||
userCentric.Contract.Metadata.Type = "mission"
|
||||
}
|
||||
|
||||
const sniperLoadouts = createSniperLoadouts(contractData)
|
||||
|
||||
if (req.gameVersion === "scpc") {
|
||||
sniperLoadouts.forEach((loadout) => {
|
||||
loadout["LoadoutData"] = loadout["Loadout"]["LoadoutData"]
|
||||
delete loadout["Loadout"]
|
||||
})
|
||||
}
|
||||
|
||||
res.json({
|
||||
template:
|
||||
req.gameVersion === "h1"
|
||||
? getConfig("LegacyPlanningTemplate", false)
|
||||
: req.gameVersion === "scpc"
|
||||
? getConfig("FrankensteinPlanningTemplate", false)
|
||||
: null,
|
||||
data: {
|
||||
Contract: contractData,
|
||||
ElusiveContractState: "",
|
||||
UserCentric: userCentric,
|
||||
IsFirstInGroup: escalation
|
||||
? controller.escalationMappings[escalationGroupId]["1"] ===
|
||||
req.query.contractid
|
||||
: true,
|
||||
Creator: creatorProfile,
|
||||
UserContract: creatorProfile.DevId !== "IOI",
|
||||
UnlockedEntrances:
|
||||
contractData.Metadata.Type === "sniper"
|
||||
? null
|
||||
: typedInv
|
||||
.filter(
|
||||
(item) =>
|
||||
item.Unlockable.Subtype ===
|
||||
"startinglocation",
|
||||
)
|
||||
.filter(
|
||||
(item) =>
|
||||
item.Unlockable.Properties.Difficulty ===
|
||||
contractData.Metadata.Difficulty,
|
||||
)
|
||||
.map((i) => i.Unlockable.Properties.RepositoryId)
|
||||
.filter((id) => id),
|
||||
UnlockedAgencyPickups:
|
||||
contractData.Metadata.Type === "sniper"
|
||||
? null
|
||||
: typedInv
|
||||
.filter(
|
||||
(item) => item.Unlockable.Type === "agencypickup",
|
||||
)
|
||||
.filter(
|
||||
(item) =>
|
||||
item.Unlockable.Properties.Difficulty ===
|
||||
contractData.Metadata.Difficulty,
|
||||
)
|
||||
.map((i) => i.Unlockable.Properties.RepositoryId)
|
||||
.filter((id) => id),
|
||||
Objectives: mapObjectives(
|
||||
contractData.Data.Objectives,
|
||||
contractData.Data.GameChangers || [],
|
||||
contractData.Metadata.GroupObjectiveDisplayOrder || [],
|
||||
),
|
||||
GroupData: groupData,
|
||||
Entrances:
|
||||
contractData.Metadata.Type === "sniper"
|
||||
? null
|
||||
: unlockedEntrances
|
||||
.filter((unlockable) =>
|
||||
entrancesInScene.includes(
|
||||
unlockable.Properties.RepositoryId,
|
||||
),
|
||||
)
|
||||
.filter(
|
||||
(unlockable) =>
|
||||
unlockable.Properties.Difficulty ===
|
||||
contractData.Metadata.Difficulty,
|
||||
)
|
||||
.sort(unlockorderComparer),
|
||||
Location: sublocation,
|
||||
LoadoutData:
|
||||
contractData.Metadata.Type === "sniper"
|
||||
? null
|
||||
: [
|
||||
{
|
||||
SlotName: "carriedweapon",
|
||||
SlotId: "0",
|
||||
Recommended: null,
|
||||
},
|
||||
{
|
||||
SlotName: "carrieditem",
|
||||
SlotId: "1",
|
||||
Recommended: null,
|
||||
},
|
||||
{
|
||||
SlotName: "concealedweapon",
|
||||
SlotId: "2",
|
||||
Recommended: {
|
||||
item:
|
||||
contractData.Peacock?.noCarriedWeapon ===
|
||||
true
|
||||
? null
|
||||
: typedInv.find(
|
||||
(item) =>
|
||||
item.Unlockable.Id ===
|
||||
pistol,
|
||||
),
|
||||
type: "concealedweapon",
|
||||
},
|
||||
},
|
||||
{
|
||||
SlotName: "disguise",
|
||||
SlotId: "3",
|
||||
Recommended: {
|
||||
item: typedInv.find(
|
||||
(item) => item.Unlockable.Id === suit,
|
||||
),
|
||||
type: "disguise",
|
||||
},
|
||||
},
|
||||
{
|
||||
SlotName: "gear",
|
||||
SlotId: "4",
|
||||
Recommended: {
|
||||
item:
|
||||
contractData.Peacock?.noGear === true
|
||||
? null
|
||||
: typedInv.find(
|
||||
(item) =>
|
||||
item.Unlockable.Id ===
|
||||
tool1,
|
||||
),
|
||||
type: "gear",
|
||||
},
|
||||
},
|
||||
{
|
||||
SlotName: "gear",
|
||||
SlotId: "5",
|
||||
Recommended: {
|
||||
item:
|
||||
contractData.Peacock?.noGear === true
|
||||
? null
|
||||
: typedInv.find(
|
||||
(item) =>
|
||||
item.Unlockable.Id ===
|
||||
tool2,
|
||||
),
|
||||
type: "gear",
|
||||
},
|
||||
},
|
||||
{
|
||||
SlotName: "stashpoint",
|
||||
SlotId: "6",
|
||||
Recommended: null,
|
||||
},
|
||||
briefcaseId && {
|
||||
SlotName: briefcaseProp,
|
||||
SlotId: briefcaseId,
|
||||
Recommended: {
|
||||
item: {
|
||||
...i,
|
||||
Properties: {},
|
||||
},
|
||||
type: i.Unlockable.Id,
|
||||
owned: true,
|
||||
},
|
||||
IsContainer: true,
|
||||
},
|
||||
].filter(Boolean),
|
||||
LimitedLoadoutUnlockLevel: 0, // Hokkaido
|
||||
CharacterLoadoutData:
|
||||
sniperLoadouts.length !== 0 ? sniperLoadouts : null,
|
||||
ChallengeData: {
|
||||
Children:
|
||||
controller.challengeService.getChallengePlanningDataForContract(
|
||||
req.query.contractid,
|
||||
req.gameVersion,
|
||||
req.jwt.unique_name,
|
||||
),
|
||||
},
|
||||
Currency: {
|
||||
Balance: 0,
|
||||
},
|
||||
PaymentDetails: {
|
||||
Currency: "Merces",
|
||||
Amount: 0,
|
||||
MaximumDeduction: 85,
|
||||
Bonuses: null,
|
||||
Expenses: null,
|
||||
Entrance: null,
|
||||
Pickup: null,
|
||||
SideMission: null,
|
||||
},
|
||||
OpportunityData: (contractData.Metadata.Opportunities || [])
|
||||
.map((value) => missionStories[value])
|
||||
.filter(Boolean),
|
||||
PlayerProfileXpData: {
|
||||
XP: userData.Extensions.progression.PlayerProfileXP.Total,
|
||||
Level: userData.Extensions.progression.PlayerProfileXP
|
||||
.ProfileLevel,
|
||||
MaxLevel: 5000,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
100
components/menus/playnext.ts
Normal file
100
components/menus/playnext.ts
Normal file
@ -0,0 +1,100 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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 { generateUserCentric } from "../contracts/dataGen"
|
||||
import { controller } from "../controller"
|
||||
import type { GameVersion, PlayNextCampaignDetails } from "../types/types"
|
||||
|
||||
export const orderedMissions: string[] = [
|
||||
"00000000-0000-0000-0000-000000000200",
|
||||
"00000000-0000-0000-0000-000000000600",
|
||||
"00000000-0000-0000-0000-000000000400",
|
||||
"db341d9f-58a4-411d-be57-0bc4ed85646b",
|
||||
"42bac555-bbb9-429d-a8ce-f1ffdf94211c",
|
||||
"0e81a82e-b409-41e9-9e3b-5f82e57f7a12",
|
||||
"c65019e5-43a8-4a33-8a2a-84c750a5eeb3",
|
||||
"c1d015b4-be08-4e44-808e-ada0f387656f",
|
||||
"422519be-ed2e-44df-9dac-18f739d44fd9",
|
||||
"0fad48d7-3d0f-4c66-8605-6cbe9c3a46d7",
|
||||
"82f55837-e26c-41bf-bc6e-fa97b7981fbc",
|
||||
"0d225edf-40cd-4f20-a30f-b62a373801d3",
|
||||
"7a03a97d-238c-48bd-bda0-e5f279569cce",
|
||||
"095261b5-e15b-4ca1-9bb7-001fb85c5aaa",
|
||||
"7d85f2b0-80ca-49be-a2b7-d56f67faf252",
|
||||
"755984a8-fb0b-4673-8637-95cfe7d34e0f",
|
||||
"ebcd14b2-0786-4ceb-a2a4-e771f60d0125",
|
||||
"3d0cbb8c-2a80-442a-896b-fea00e98768c",
|
||||
"d42f850f-ca55-4fc9-9766-8c6a2b5c3129",
|
||||
"a3e19d55-64a6-4282-bb3c-d18c3f3e6e29",
|
||||
]
|
||||
|
||||
/**
|
||||
* Gets the ID for a season.
|
||||
*
|
||||
* @param index The index in orderedMissions.
|
||||
* @returns The season's ID. ("1", "2", or "3")
|
||||
* @see orderedMissions
|
||||
*/
|
||||
export function getSeasonId(index: number): string {
|
||||
if (index <= 5) {
|
||||
return "1"
|
||||
}
|
||||
|
||||
if (index <= 13) {
|
||||
return "2"
|
||||
}
|
||||
|
||||
return "3"
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a tile for play next given a contract ID and other details.
|
||||
*
|
||||
* @param userId The user's ID.
|
||||
* @param contractId The next contract ID.
|
||||
* @param gameVersion The game version.
|
||||
* @param campaignInfo The campaign information.
|
||||
* @returns The tile object.
|
||||
*/
|
||||
export function createPlayNextTile(
|
||||
userId: string,
|
||||
contractId: string,
|
||||
gameVersion: GameVersion,
|
||||
campaignInfo: PlayNextCampaignDetails,
|
||||
) {
|
||||
return {
|
||||
CategoryType: "NextMission",
|
||||
CategoryName: "UI_PLAYNEXT_CONTINUE_STORY_TITLE",
|
||||
Items: [
|
||||
{
|
||||
ItemType: null,
|
||||
ContentType: "Contract",
|
||||
Content: {
|
||||
ContractId: contractId,
|
||||
UserCentricContract: generateUserCentric(
|
||||
controller.resolveContract(contractId),
|
||||
userId,
|
||||
gameVersion,
|
||||
),
|
||||
CampaignInfo: campaignInfo,
|
||||
},
|
||||
CategoryType: "NextMission",
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
138
components/menus/sniper.ts
Normal file
138
components/menus/sniper.ts
Normal file
@ -0,0 +1,138 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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 { getConfig } from "../configSwizzleManager"
|
||||
import type {
|
||||
CompletionData,
|
||||
MissionManifest,
|
||||
SniperLoadout,
|
||||
} from "../types/types"
|
||||
|
||||
export type SniperLoadoutConfig = {
|
||||
[locationId: string]: {
|
||||
[firearmCharacter: string]: SniperLoadout
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the sniper loadouts data.
|
||||
*
|
||||
* @author Anthony Fuller
|
||||
* @param contractData The contract's data.
|
||||
* @param loadoutData Should the output just contain loadout data in an array?
|
||||
* @returns The sniper loadouts data.
|
||||
*/
|
||||
export function createSniperLoadouts(
|
||||
contractData: MissionManifest,
|
||||
loadoutData = false,
|
||||
) {
|
||||
const sniperLoadouts = []
|
||||
|
||||
if (contractData.Metadata.Type === "sniper") {
|
||||
const sLoadouts = getConfig<SniperLoadoutConfig>("SniperLoadouts", true)
|
||||
|
||||
for (const index in sLoadouts[contractData.Metadata.Location]) {
|
||||
const character = sLoadouts[contractData.Metadata.Location][index]
|
||||
const data = {
|
||||
Id: character.ID,
|
||||
Loadout: {
|
||||
LoadoutData: [
|
||||
{
|
||||
SlotId: "0",
|
||||
SlotName: "carriedweapon",
|
||||
Items: [
|
||||
{
|
||||
Item: {
|
||||
InstanceId: character.InstanceID,
|
||||
ProfileId:
|
||||
"00000000-0000-0000-0000-000000000000",
|
||||
Unlockable: character.Unlockable,
|
||||
Properties: {},
|
||||
},
|
||||
ItemDetails: {
|
||||
Capabilities: [],
|
||||
StatList: [
|
||||
{
|
||||
Name: "clipsize",
|
||||
Ratio: 0.2,
|
||||
},
|
||||
{
|
||||
Name: "damage",
|
||||
Ratio: 1.0,
|
||||
},
|
||||
{
|
||||
Name: "range",
|
||||
Ratio: 1.0,
|
||||
},
|
||||
{
|
||||
Name: "rateoffire",
|
||||
Ratio: 0.3,
|
||||
},
|
||||
],
|
||||
PropertyTexts: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
Page: 0,
|
||||
Recommended: {
|
||||
item: {
|
||||
InstanceId:
|
||||
"00000000-0000-0000-0000-000000000000",
|
||||
ProfileId:
|
||||
"00000000-0000-0000-0000-000000000000",
|
||||
Unlockable: character.Unlockable,
|
||||
Properties: {},
|
||||
},
|
||||
type: "carriedweapon",
|
||||
owned: true,
|
||||
},
|
||||
HasMore: false,
|
||||
HasMoreLeft: false,
|
||||
HasMoreRight: false,
|
||||
OptionalData: {},
|
||||
},
|
||||
],
|
||||
LimitedLoadoutUnlockLevel: 0 as number | undefined,
|
||||
},
|
||||
// TODO(Anthony): Use the function to get these details.
|
||||
CompletionData: {
|
||||
Level: 20,
|
||||
MaxLevel: 20,
|
||||
XP: 0,
|
||||
Completion: 1,
|
||||
XpLeft: 0,
|
||||
Id: index,
|
||||
SubLocationId: "",
|
||||
HideProgression: false,
|
||||
IsLocationProgression: false,
|
||||
Name: index,
|
||||
} as CompletionData,
|
||||
}
|
||||
|
||||
if (loadoutData) {
|
||||
delete data.Loadout.LimitedLoadoutUnlockLevel
|
||||
sniperLoadouts.push(data.Loadout)
|
||||
continue
|
||||
}
|
||||
|
||||
sniperLoadouts.push(data)
|
||||
}
|
||||
}
|
||||
|
||||
return sniperLoadouts
|
||||
}
|
163
components/multiplayer/multiplayerMenuData.ts
Normal file
163
components/multiplayer/multiplayerMenuData.ts
Normal file
@ -0,0 +1,163 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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 { RequestWithJwt } from "../types/types"
|
||||
import { contractSessions } from "../eventHandler"
|
||||
import { getUserData } from "../databaseHandler"
|
||||
import {
|
||||
calculateMpScore,
|
||||
getMultiplayerLoadoutData,
|
||||
MultiplayerScore,
|
||||
} from "./multiplayerUtils"
|
||||
import { Router } from "express"
|
||||
import { getConfig } from "../configSwizzleManager"
|
||||
import { MultiplayerPreset } from "./multiplayerService"
|
||||
import { generateUserCentric } from "components/contracts/dataGen"
|
||||
import { controller } from "../controller"
|
||||
import {
|
||||
MissionEndRequestQuery,
|
||||
MultiplayerMatchStatsQuery,
|
||||
MultiplayerQuery,
|
||||
} from "../types/gameSchemas"
|
||||
|
||||
export const multiplayerMenuDataRouter = Router()
|
||||
|
||||
multiplayerMenuDataRouter.post(
|
||||
"/multiplayermatchstatsready",
|
||||
(req: RequestWithJwt<MissionEndRequestQuery>, res) => {
|
||||
res.json({
|
||||
template: null,
|
||||
data: {
|
||||
contractSessionId: req.query.contractSessionId,
|
||||
isReady: true,
|
||||
retryCount: 1,
|
||||
},
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
multiplayerMenuDataRouter.post(
|
||||
"/multiplayermatchstats",
|
||||
(req: RequestWithJwt<MultiplayerMatchStatsQuery>, res) => {
|
||||
const sessionDetails = contractSessions.get(req.query.contractSessionId)
|
||||
|
||||
if (!sessionDetails) {
|
||||
// contract session not found
|
||||
res.status(404).end()
|
||||
return
|
||||
}
|
||||
|
||||
const scores: MultiplayerScore[] = [calculateMpScore(sessionDetails)]
|
||||
|
||||
if (!sessionDetails.ghost) {
|
||||
throw new Error("no mp details on mp session")
|
||||
}
|
||||
|
||||
for (const opponentId in sessionDetails.ghost.Opponents) {
|
||||
const opponentSessionDetails = contractSessions.get(
|
||||
sessionDetails.ghost.Opponents[opponentId],
|
||||
)
|
||||
|
||||
if (opponentSessionDetails) {
|
||||
scores.push(calculateMpScore(opponentSessionDetails))
|
||||
} else {
|
||||
// opponent contract session not found
|
||||
scores.push({})
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
template: null,
|
||||
data: {
|
||||
Players: scores,
|
||||
},
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
interface MultiplayerPresetsQuery {
|
||||
gamemode?: string
|
||||
disguiseUnlockableId?: string
|
||||
}
|
||||
|
||||
multiplayerMenuDataRouter.get(
|
||||
"/multiplayerpresets",
|
||||
(req: RequestWithJwt<MultiplayerPresetsQuery>, res) => {
|
||||
if (req.query.gamemode !== "versus") {
|
||||
res.status(401).send("unknown gamemode")
|
||||
return
|
||||
}
|
||||
|
||||
const presets = getConfig<MultiplayerPreset[]>(
|
||||
"MultiplayerPresets",
|
||||
false,
|
||||
)
|
||||
|
||||
const userData = getUserData(req.jwt.unique_name, req.gameVersion)
|
||||
|
||||
const contractIds: Set<string> = new Set()
|
||||
|
||||
for (const preset of presets) {
|
||||
for (const contractId of preset.Data.Contracts) {
|
||||
contractIds.add(contractId)
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
template: null,
|
||||
data: {
|
||||
Presets: presets,
|
||||
UserCentricContracts: [...contractIds].map((contractId) => {
|
||||
return generateUserCentric(
|
||||
controller.resolveContract(contractId),
|
||||
req.jwt.unique_name,
|
||||
req.gameVersion,
|
||||
)
|
||||
}),
|
||||
LoadoutData: getMultiplayerLoadoutData(
|
||||
userData,
|
||||
req.query.disguiseUnlockableId,
|
||||
req.gameVersion,
|
||||
),
|
||||
},
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
multiplayerMenuDataRouter.get(
|
||||
"/multiplayer",
|
||||
(req: RequestWithJwt<MultiplayerQuery>, res) => {
|
||||
// /multiplayer?gamemode=versus&disguiseUnlockableId=TOKEN_OUTFIT_ELUSIVE_COMPLETE_15_SUIT
|
||||
if (req.query.gamemode !== "versus") {
|
||||
return
|
||||
}
|
||||
|
||||
const userData = getUserData(req.jwt.unique_name, req.gameVersion)
|
||||
|
||||
res.json({
|
||||
template: null,
|
||||
data: {
|
||||
LoadoutData: getMultiplayerLoadoutData(
|
||||
userData,
|
||||
req.query.disguiseUnlockableId,
|
||||
req.gameVersion,
|
||||
),
|
||||
},
|
||||
})
|
||||
},
|
||||
)
|
280
components/multiplayer/multiplayerService.ts
Normal file
280
components/multiplayer/multiplayerService.ts
Normal file
@ -0,0 +1,280 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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 { log, LogLevel } from "../loggingInterop"
|
||||
import { Router } from "express"
|
||||
import { enqueuePushMessage } from "../eventHandler"
|
||||
import { json as jsonMiddleware } from "body-parser"
|
||||
import {
|
||||
ClientToServerEvent,
|
||||
ContractSession,
|
||||
RequestWithJwt,
|
||||
UserCentricContract,
|
||||
} from "../types/types"
|
||||
import { nilUuid } from "../utils"
|
||||
import { randomUUID } from "crypto"
|
||||
import { getConfig } from "../configSwizzleManager"
|
||||
import { generateUserCentric } from "../contracts/dataGen"
|
||||
import { controller } from "../controller"
|
||||
import { MatchOverC2SEvent } from "../types/events"
|
||||
|
||||
/**
|
||||
* A multiplayer preset.
|
||||
*/
|
||||
export interface MultiplayerPreset {
|
||||
/**
|
||||
* The preset's ID.
|
||||
*/
|
||||
Id: string
|
||||
/**
|
||||
* The preset's game mode.
|
||||
*/
|
||||
GameMode: "versus" | string
|
||||
Metadata: {
|
||||
Title: string
|
||||
Header: string
|
||||
Image: string
|
||||
IsDefault: boolean
|
||||
}
|
||||
Data: {
|
||||
Contracts: string[]
|
||||
Properties: {
|
||||
mode: string
|
||||
active: boolean
|
||||
__comment?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface MatchData {
|
||||
Players: string[]
|
||||
MatchData: {
|
||||
contractId: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension for a session providing ghost mode details.
|
||||
*/
|
||||
export interface SessionGhostModeDetails {
|
||||
deaths: number
|
||||
unnoticedKills: number
|
||||
Opponents: string[]
|
||||
IsWinner: boolean
|
||||
Score: number
|
||||
OpponentScore: number
|
||||
IsDraw: boolean
|
||||
timerEnd: number | null
|
||||
}
|
||||
|
||||
export const multiplayerRouter = Router()
|
||||
const activeMatches: Map<string, MatchData> = new Map()
|
||||
|
||||
multiplayerRouter.post(
|
||||
"/GetRequiredResourcesForPreset",
|
||||
jsonMiddleware(),
|
||||
(req: RequestWithJwt, res) => {
|
||||
const allPresets = getConfig<MultiplayerPreset[]>(
|
||||
"MultiplayerPresets",
|
||||
false,
|
||||
)
|
||||
|
||||
const requestedPreset = allPresets.find(
|
||||
(preset) => preset.Id === req.body.id,
|
||||
)
|
||||
|
||||
if (!requestedPreset) {
|
||||
res.status(404).end()
|
||||
log(LogLevel.WARN, "unknown multiplayer preset id requested")
|
||||
return
|
||||
}
|
||||
|
||||
const contractIds = requestedPreset.Data.Contracts
|
||||
const userCentrics = contractIds
|
||||
.map((id) =>
|
||||
generateUserCentric(
|
||||
controller.resolveContract(id),
|
||||
req.jwt.unique_name,
|
||||
req.gameVersion,
|
||||
),
|
||||
)
|
||||
.filter(Boolean)
|
||||
|
||||
res.json(
|
||||
userCentrics.map((userCentric: UserCentricContract) => ({
|
||||
Id: userCentric.Contract.Metadata.Id,
|
||||
DlcId: userCentric.Data.DlcName,
|
||||
Resources: [
|
||||
userCentric.Contract.Metadata.ScenePath,
|
||||
...(userCentric.Contract.Data.Bricks ?? []),
|
||||
],
|
||||
})),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
multiplayerRouter.post(
|
||||
"/RegisterToMatch",
|
||||
jsonMiddleware(),
|
||||
(req: RequestWithJwt, res) => {
|
||||
// get a random contract from the list of possible ones in the selected preset
|
||||
const multiplayerPresets = getConfig<MultiplayerPreset[]>(
|
||||
"MultiplayerPresets",
|
||||
false,
|
||||
)
|
||||
|
||||
if (!req.body.presetId) {
|
||||
req.body.presetId = "d72d7cc9-ee26-4c7d-857a-75abdc9ccb61" // default to miami invite preset
|
||||
}
|
||||
|
||||
const preset = multiplayerPresets.find(
|
||||
(preset) => preset.Id === req.body.presetId,
|
||||
)
|
||||
|
||||
if (!preset) {
|
||||
res.status(404).end()
|
||||
log(
|
||||
LogLevel.WARN,
|
||||
`Unknown preset id requested (${req.body.presetId})`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const contractId =
|
||||
preset.Data.Contracts[
|
||||
Math.trunc(Math.random() * preset.Data.Contracts.length)
|
||||
]
|
||||
|
||||
if (req.body.matchId === nilUuid) {
|
||||
// create new match
|
||||
req.body.matchId = randomUUID()
|
||||
activeMatches.set(req.body.matchId, {
|
||||
MatchData: {
|
||||
contractId: contractId,
|
||||
},
|
||||
Players: [req.jwt.unique_name],
|
||||
})
|
||||
} else if (activeMatches.has(req.body.matchId)) {
|
||||
// join existing match
|
||||
const match = activeMatches.get(req.body.matchId)!
|
||||
|
||||
match.Players.forEach((playerId) =>
|
||||
enqueuePushMessage(playerId, {
|
||||
MatchId: req.body.matchId,
|
||||
Type: 1,
|
||||
PlayerId: req.jwt.unique_name,
|
||||
MatchData: null,
|
||||
}),
|
||||
)
|
||||
|
||||
match.Players.push(req.jwt.unique_name)
|
||||
} else {
|
||||
// MatchId not found
|
||||
res.status(404).end()
|
||||
return
|
||||
}
|
||||
|
||||
enqueuePushMessage(req.jwt.unique_name, {
|
||||
MatchId: req.body.matchId,
|
||||
Type: 3,
|
||||
PlayerId: nilUuid,
|
||||
MatchData: activeMatches.get(req.body.matchId)!.MatchData,
|
||||
})
|
||||
|
||||
res.json({
|
||||
MatchId: req.body.matchId,
|
||||
PreferedHostIndex: 0,
|
||||
Tickets: [],
|
||||
MatchMode: null,
|
||||
MatchData: null,
|
||||
MatchStats: {},
|
||||
MatchType: 0,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
multiplayerRouter.post(
|
||||
"/SetMatchData",
|
||||
jsonMiddleware(),
|
||||
(req: RequestWithJwt, res) => {
|
||||
const match = activeMatches.get(req.body.matchId)
|
||||
|
||||
if (!(match && match.Players.includes(req.jwt.unique_name))) {
|
||||
res.status(404).end()
|
||||
return
|
||||
}
|
||||
|
||||
match.MatchData[req.body.key] = req.body.value
|
||||
res.json({
|
||||
MatchId: req.body.matchId,
|
||||
PreferedHostIndex: 0,
|
||||
Tickets: [],
|
||||
MatchMode: null,
|
||||
MatchData: match.MatchData,
|
||||
MatchStats: {},
|
||||
MatchType: 0,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
multiplayerRouter.post("/RegisterToPreset", jsonMiddleware(), (req, res) => {
|
||||
// matchmaking
|
||||
// TODO: implement matchmaking
|
||||
// req.body.presetId
|
||||
// req.body.lobbyId (this is just a timestamp?)
|
||||
res.status(500).end()
|
||||
})
|
||||
|
||||
export function handleMultiplayerEvent(
|
||||
event: ClientToServerEvent,
|
||||
session: ContractSession,
|
||||
): boolean {
|
||||
const emptySession = <SessionGhostModeDetails>{}
|
||||
const ghost = session.ghost || emptySession
|
||||
|
||||
switch (event.Name) {
|
||||
case "Ghost_PlayerDied":
|
||||
ghost.deaths += 1
|
||||
return true
|
||||
case "Ghost_TargetUnnoticed":
|
||||
ghost.unnoticedKills += 1
|
||||
return true
|
||||
case "Opponents": {
|
||||
const value = event.Value as {
|
||||
ConnectedSessions: string[]
|
||||
}
|
||||
|
||||
ghost.Opponents = value.ConnectedSessions
|
||||
return true
|
||||
}
|
||||
case "MatchOver": {
|
||||
const matchOverValue = (event as MatchOverC2SEvent).Value
|
||||
|
||||
ghost.Score = matchOverValue.MyScore
|
||||
ghost.OpponentScore = matchOverValue.OpponentScore
|
||||
ghost.IsWinner = matchOverValue.IsWinner
|
||||
ghost.IsDraw = matchOverValue.IsDraw
|
||||
ghost.timerEnd = event.Timestamp
|
||||
|
||||
return true
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
185
components/multiplayer/multiplayerUtils.ts
Normal file
185
components/multiplayer/multiplayerUtils.ts
Normal file
@ -0,0 +1,185 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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 {
|
||||
ContractSession,
|
||||
GameVersion,
|
||||
Unlockable,
|
||||
UserProfile,
|
||||
} from "../types/types"
|
||||
import { getVersionedConfig } from "../configSwizzleManager"
|
||||
import { createInventory } from "../inventory"
|
||||
import { randomUUID } from "crypto"
|
||||
import { nilUuid } from "../utils"
|
||||
import { log, LogLevel } from "../loggingInterop"
|
||||
import assert from "assert"
|
||||
|
||||
export interface MultiplayerScore {
|
||||
Header?: {
|
||||
GameMode: "Ghost" | string
|
||||
Result: "Win" | "Loss" | string
|
||||
}
|
||||
Metadata?: Record<string, never>
|
||||
Data?: {
|
||||
Score: number
|
||||
OpponentScore: number
|
||||
PacifiedNpcs: number
|
||||
DisguisesUsed: number
|
||||
DisguisesRuined: number
|
||||
BodiesHidden: number
|
||||
UnnoticedKills: number
|
||||
KilledNpcs: number
|
||||
Deaths: number
|
||||
Duration: number | Date
|
||||
}
|
||||
}
|
||||
|
||||
export function calculateMpScore(
|
||||
sessionDetails: ContractSession,
|
||||
): MultiplayerScore {
|
||||
if (!sessionDetails.ghost) {
|
||||
throw new Error("no mp details on mp session")
|
||||
}
|
||||
|
||||
return {
|
||||
Header: {
|
||||
GameMode: "Ghost",
|
||||
Result: sessionDetails.ghost.IsWinner ? "Win" : "Loss",
|
||||
},
|
||||
Metadata: {},
|
||||
Data: {
|
||||
Score: sessionDetails.ghost.Score,
|
||||
OpponentScore: sessionDetails.ghost.OpponentScore,
|
||||
PacifiedNpcs: [...sessionDetails.pacifications].filter(
|
||||
(id) =>
|
||||
!sessionDetails.npcKills.has(id) &&
|
||||
!sessionDetails.targetKills.has(id),
|
||||
).length,
|
||||
DisguisesUsed: sessionDetails.disguisesUsed.size,
|
||||
DisguisesRuined: sessionDetails.disguisesRuined.size,
|
||||
BodiesHidden: sessionDetails.bodiesHidden.size,
|
||||
UnnoticedKills: sessionDetails.ghost.unnoticedKills,
|
||||
KilledNpcs:
|
||||
sessionDetails.npcKills.size + sessionDetails.crowdNpcKills,
|
||||
Deaths: sessionDetails.ghost.deaths,
|
||||
Duration: sessionDetails.duration,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function getMultiplayerLoadoutData(
|
||||
userData: UserProfile,
|
||||
disguiseUnlockableId: string,
|
||||
gameVersion: GameVersion,
|
||||
) {
|
||||
const allunlockables = getVersionedConfig<Unlockable[]>(
|
||||
"allunlockables",
|
||||
gameVersion,
|
||||
false,
|
||||
)
|
||||
|
||||
const inventory = createInventory(
|
||||
userData.Id,
|
||||
gameVersion,
|
||||
userData.Extensions.entP,
|
||||
)
|
||||
|
||||
let unlockable = inventory.find(
|
||||
(unlockable) =>
|
||||
unlockable.Unlockable.Id === disguiseUnlockableId &&
|
||||
unlockable.Unlockable.Type === "disguise",
|
||||
)?.Unlockable
|
||||
|
||||
if (!unlockable) {
|
||||
unlockable = allunlockables.find(
|
||||
(unlockable) => unlockable.Id === "TOKEN_OUTFIT_HITMANSUIT",
|
||||
)
|
||||
|
||||
assert.ok(unlockable)
|
||||
}
|
||||
|
||||
unlockable.GameAsset = null
|
||||
unlockable.DisplayNameLocKey = `UI_${unlockable.Id}_NAME`
|
||||
|
||||
return [
|
||||
{
|
||||
SlotName: "disguise",
|
||||
SlotId: "3",
|
||||
Recommended: {
|
||||
item: {
|
||||
InstanceId: randomUUID(),
|
||||
ProfileId: nilUuid,
|
||||
Unlockable: unlockable,
|
||||
Properties: {},
|
||||
},
|
||||
type: "disguise",
|
||||
owned: true,
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export function encodePushMessage<T>(timestamp: bigint, message: T): string {
|
||||
const msgstr = JSON.stringify(message)
|
||||
const msglength = Buffer.byteLength(msgstr, "utf8")
|
||||
let totallength = msglength + 8 + 80 // using a fixed length of 8 for the timestamp for now...
|
||||
totallength += 4 - (totallength % 4) // pad to the nearest multiple of 4
|
||||
const output = Buffer.alloc(totallength)
|
||||
let offset = 0
|
||||
|
||||
offset = output.writeUInt32LE(totallength, offset)
|
||||
// no idea what these first two chunks are for
|
||||
offset = output.writeUInt32LE(0x0000000c, offset)
|
||||
offset = output.writeUInt16LE(0x0008, offset)
|
||||
offset = output.writeUInt16LE(0x000e, offset)
|
||||
offset = output.writeUInt16LE(0x0007, offset)
|
||||
offset = output.writeUInt16LE(0x0008, offset)
|
||||
offset = output.writeUInt32LE(0x00000008, offset)
|
||||
offset = output.writeUInt32BE(0x00000002, offset)
|
||||
|
||||
offset = output.writeUInt32LE(0x00000014, offset)
|
||||
offset = output.writeUInt16LE(0x0000, offset)
|
||||
offset = output.writeUInt16LE(0x000e, offset)
|
||||
offset = output.writeUInt16LE(0x0014, offset)
|
||||
offset = output.writeUInt16LE(0x0006, offset)
|
||||
offset = output.writeUInt16LE(0x0000, offset)
|
||||
offset = output.writeUInt16LE(0x0005, offset)
|
||||
offset = output.writeUInt16LE(0x0008, offset)
|
||||
offset = output.writeUInt16LE(0x000c, offset)
|
||||
offset = output.writeUInt32LE(0x0000000e, offset)
|
||||
offset = output.writeUInt32BE(0x00010300, offset)
|
||||
|
||||
offset = output.writeUInt32LE(0x0c + 8, offset)
|
||||
offset = output.writeBigUInt64LE(timestamp, offset)
|
||||
offset = output.writeUInt16LE(0x0008, offset)
|
||||
offset = output.writeUInt16LE(0x000c, offset)
|
||||
offset = output.writeUInt16LE(0x0006, offset)
|
||||
offset = output.writeUInt16LE(0x0008, offset)
|
||||
offset = output.writeUInt32LE(0x00000008, offset)
|
||||
offset = output.writeUInt32BE(0x00008300, offset)
|
||||
|
||||
offset = output.writeUInt32LE(0x04, offset)
|
||||
offset = output.writeUInt32LE(msglength, offset)
|
||||
offset = output.write(msgstr, offset, "utf8")
|
||||
|
||||
if (PEACOCK_DEV) {
|
||||
log(LogLevel.DEBUG, `Encoded message offset: ${offset}`)
|
||||
}
|
||||
|
||||
return output.toString("base64")
|
||||
}
|
310
components/oauthToken.ts
Normal file
310
components/oauthToken.ts
Normal file
@ -0,0 +1,310 @@
|
||||
/*
|
||||
* The Peacock Project - a HITMAN server replacement.
|
||||
* Copyright (C) 2021-2022 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 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 { log, LogLevel } from "./loggingInterop"
|
||||
import {
|
||||
STEAM_NAMESPACE_2018,
|
||||
STEAM_NAMESPACE_2021,
|
||||
} from "./platformEntitlements"
|
||||
import {
|
||||
getExternalUserData,
|
||||
getUserData,
|
||||
loadUserData,
|
||||
writeExternalUserData,
|
||||
writeNewUserData,
|
||||
} from "./databaseHandler"
|
||||
import { OfficialServerAuth, userAuths } from "./officialServerAuth"
|
||||
import { randomUUID } from "crypto"
|
||||
import { getFlag } from "./flags"
|
||||
import { clearInventoryFor } from "./inventory"
|
||||
import {
|
||||
EpicH1Strategy,
|
||||
EpicH3Strategy,
|
||||
IOIStrategy,
|
||||
SteamH1Strategy,
|
||||
SteamH2Strategy,
|
||||
SteamScpcStrategy,
|
||||
} from "./entitlementStrategies"
|
||||
|
||||
export async function handleOauthToken(
|
||||
req: RequestWithJwt,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const isFrankenstein = req.body.gs === "scpc-prod"
|
||||
|
||||
const signOptions = {
|
||||
notBefore: -60000,
|
||||
expiresIn: 6000,
|
||||
issuer: "auth.hitman.io",
|
||||
audience: isFrankenstein ? "scpc-prod" : "pc_prod_8",
|
||||
noTimestamp: true,
|
||||
}
|
||||
|
||||
//#region Refresh tokens
|
||||
if (req.body.grant_type === "refresh_token") {
|
||||
// send back the token from the request (re-signed so the timestamps update)
|
||||
extractToken(req) // init req.jwt
|
||||
// remove signOptions from existing jwt
|
||||
// ts-expect-error Non-optional, we're reassigning.
|
||||
delete req.jwt.nbf // notBefore
|
||||
// ts-expect-error Non-optional, we're reassigning.
|
||||
delete req.jwt.exp // expiresIn
|
||||
// ts-expect-error Non-optional, we're reassigning.
|
||||
delete req.jwt.iss // issuer
|
||||
// ts-expect-error Non-optional, we're reassigning.
|
||||
delete req.jwt.aud // audience
|
||||
|
||||
if (getFlag("officialAuthentication") === true && !isFrankenstein) {
|
||||
if (userAuths.has(req.jwt.unique_name)) {
|
||||
userAuths
|
||||
.get(req.jwt.unique_name)!
|
||||
._doRefresh()
|
||||
.then(() => undefined)
|
||||
.catch(() => {
|
||||
log(LogLevel.WARN, "Failed authentication refresh.")
|
||||
userAuths.get(req.jwt.unique_name)!.initialized = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
access_token: sign(req.jwt, "secret", signOptions),
|
||||
token_type: "bearer",
|
||||
expires_in: 5000,
|
||||
refresh_token: randomUUID(),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
//#endregion
|
||||
|
||||
let external_platform: "steam" | "epic",
|
||||
external_userid: string,
|
||||
external_users_folder: "steamids" | "epicids",
|
||||
external_appid: string
|
||||
|
||||
if (req.body.grant_type === "external_steam") {
|
||||
if (!/^\d{1,20}$/.test(req.body.steam_userid)) {
|
||||
res.status(400).end() // invalid steam user id
|
||||
return
|
||||
}
|
||||
|
||||
external_platform = "steam"
|
||||
external_userid = req.body.steam_userid
|
||||
external_users_folder = "steamids"
|
||||
external_appid = req.body.steam_appid
|
||||
} else if (req.body.grant_type === "external_epic") {
|
||||
if (!/^[\da-f]{32}$/.test(req.body.epic_userid)) {
|
||||
res.status(400).end() // invalid epic user id
|
||||
return
|
||||
}
|
||||
|
||||
const epic_token = decode(
|
||||
req.body.access_token.replace(/^eg1~/, ""),
|
||||
) as {
|
||||
appid: string
|
||||
app: string
|
||||
}
|
||||
|
||||
if (!epic_token || !(epic_token.appid || epic_token.app)) {
|
||||
res.status(400).end() // invalid epic access token
|
||||
return
|
||||
}
|
||||
|
||||
external_appid = epic_token.appid || epic_token.app
|
||||
external_platform = "epic"
|
||||
external_userid = req.body.epic_userid
|
||||
external_users_folder = "epicids"
|
||||
} else {
|
||||
res.status(406).end() // unsupported auth method
|
||||
return
|
||||
}
|
||||
|
||||
if (req.body.pId && !uuidRegex.test(req.body.pId)) {
|
||||
res.status(400).end() // pId is not a GUID
|
||||
return
|
||||
}
|
||||
|
||||
const isHitman3 =
|
||||
external_appid === "fghi4567xQOCheZIin0pazB47qGUvZw4" ||
|
||||
external_appid === STEAM_NAMESPACE_2021
|
||||
|
||||
const gameVersion: GameVersion = isFrankenstein
|
||||
? "scpc"
|
||||
: isHitman3
|
||||
? "h3"
|
||||
: external_appid === STEAM_NAMESPACE_2018
|
||||
? "h2"
|
||||
: "h1"
|
||||
|
||||
if (!req.body.pId) {
|
||||
// if no profile id supplied
|
||||
try {
|
||||
req.body.pId = (
|
||||
await getExternalUserData(
|
||||
external_userid,
|
||||
external_users_folder,
|
||||
gameVersion,
|
||||
)
|
||||
).toString()
|
||||
} catch (e) {
|
||||
req.body.pId = randomUUID()
|
||||
await writeExternalUserData(
|
||||
external_userid,
|
||||
external_users_folder,
|
||||
req.body.pId,
|
||||
gameVersion,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// if a profile id is supplied
|
||||
getExternalUserData(external_userid, external_users_folder, gameVersion)
|
||||
.then(() => null)
|
||||
.catch(async () => {
|
||||
// external id is not yet linked to this profile
|
||||
await writeExternalUserData(
|
||||
external_userid,
|
||||
external_users_folder,
|
||||
req.body.pId,
|
||||
gameVersion,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
await loadUserData(req.body.pId, gameVersion)
|
||||
} catch (e) {
|
||||
log(LogLevel.DEBUG, "Unable to load profile information.")
|
||||
}
|
||||
|
||||
if (getFlag("officialAuthentication") === true && !isFrankenstein) {
|
||||
const authContainer = new OfficialServerAuth(
|
||||
gameVersion,
|
||||
req.body.access_token,
|
||||
)
|
||||
|
||||
log(LogLevel.DEBUG, `Setting up container with ID ${req.body.pId}.`)
|
||||
|
||||
userAuths.set(req.body.pId, authContainer)
|
||||
|
||||
await authContainer._initiallyAuthenticate(req)
|
||||
}
|
||||
|
||||
if (getUserData(req.body.pId, gameVersion) === undefined) {
|
||||
// User does not exist, create new profile from default:
|
||||
log(LogLevel.DEBUG, `Create new profile ${req.body.pId}`)
|
||||
|
||||
const userData = getVersionedConfig(
|
||||
"UserDefault",
|
||||
gameVersion,
|
||||
true,
|
||||
) as UserProfile
|
||||
userData.Id = req.body.pId
|
||||
userData.LinkedAccounts[external_platform] = external_userid
|
||||
|
||||
if (external_platform === "steam") {
|
||||
userData.SteamId = req.body.steam_userid
|
||||
} else if (external_platform === "epic") {
|
||||
userData.EpicId = req.body.epic_userid
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-inner-declarations
|
||||
async function getEntitlements(): Promise<string[]> {
|
||||
if (isFrankenstein) {
|
||||
return new SteamScpcStrategy().get()
|
||||
}
|
||||
|
||||
if (gameVersion === "h1") {
|
||||
if (external_platform === "steam") {
|
||||
return new SteamH1Strategy().get()
|
||||
} else if (external_platform === "epic") {
|
||||
return new EpicH1Strategy().get()
|
||||
} else {
|
||||
log(LogLevel.ERROR, "Unsupported platform.")
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
if (gameVersion === "h2") {
|
||||
return new SteamH2Strategy().get()
|
||||
}
|
||||
|
||||
if (gameVersion === "h3") {
|
||||
if (external_platform === "epic") {
|
||||
return await new EpicH3Strategy().get(
|
||||
req.body.access_token,
|
||||
req.body.epic_userid,
|
||||
)
|
||||
} else if (external_platform === "steam") {
|
||||
return await new IOIStrategy(
|
||||
gameVersion,
|
||||
STEAM_NAMESPACE_2021,
|
||||
).get(req.body.pId)
|
||||
} else {
|
||||
log(LogLevel.ERROR, "Unsupported platform.")
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
log(LogLevel.ERROR, "Unsupported platform.")
|
||||
return []
|
||||
}
|
||||
|
||||
userData.Extensions.entP = await getEntitlements()
|
||||
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(
|
||||
userData.Extensions,
|
||||
"inventory",
|
||||
)
|
||||
) {
|
||||
// @ts-expect-error No longer in the typedefs.
|
||||
delete userData.Extensions.inventory
|
||||
}
|
||||
|
||||
writeNewUserData(req.body.pId, userData, gameVersion)
|
||||
}
|
||||
|
||||
// Format here follows steam_external, Epic jwt has some different fields
|
||||
const userinfo = {
|
||||
"auth:method": req.body.grant_type,
|
||||
roles: "user",
|
||||
sub: req.body.pId,
|
||||
unique_name: req.body.pId,
|
||||
userid: external_userid,
|
||||
platform: external_platform,
|
||||
locale: req.body.locale,
|
||||
rgn: req.body.rgn,
|
||||
pis: external_appid,
|
||||
cntry: req.body.locale,
|
||||
}
|
||||
|
||||
clearInventoryFor(req.body.pId)
|
||||
|
||||
res!.json({
|
||||
access_token: sign(userinfo, "secret", signOptions),
|
||||
token_type: "bearer",
|
||||
expires_in: 5000,
|
||||
refresh_token: randomUUID(),
|
||||
})
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user