20 Commits

Author SHA1 Message Date
b9feec2ee4 fix: handle assassination-only nodes in sortie generation
All checks were successful
Build / build (pull_request) Successful in 2m10s
2025-11-21 19:53:59 +01:00
6606e3b924 chore: update package name (#3059)
All checks were successful
Build / build (push) Successful in 1m8s
Build Docker image / docker (push) Successful in 1m58s
"wf-emulator" used to be the name of the GH repo in the very old days and was later changed to SpaceNinjaServer for *reasons*. Thought it might make sense to update it here because nowadays we call it "SpaceNinjaServer" (SNS) in the community and not "WF Emulator" anymore.

Reviewed-on: #3059
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-11-20 23:24:17 -08:00
45609d999c fix: don't throw if removeRequiredItems fails to remove an item (#3058)
Some checks failed
Build / build (push) Has been cancelled
Build Docker image / docker (push) Has been cancelled
Closes #3052

Reviewed-on: #3058
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-11-20 23:23:52 -08:00
a3c1dcd1f1 fix: create NORMAL loadout with post-tutorial accounts (#3057)
Some checks failed
Build / build (push) Has been cancelled
Build Docker image / docker (push) Has been cancelled
There are some strange client bugs resulting from logging in to an account without a loadout in post-tutorial state.

Reviewed-on: #3057
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-11-20 23:23:37 -08:00
3f3fe0b466 chore: remove license field from package.json
All checks were successful
Build / build (push) Successful in 1m16s
Build Docker image / docker (push) Successful in 1m58s
2025-11-20 14:55:26 +01:00
412ce82aae chore: fix wording of error when an item goes negative (#3054)
All checks were successful
Build / build (push) Successful in 1m4s
Build Docker image / docker (push) Successful in 4m37s
It would say "subtracting -1" instead of "adding -1" or "subtracting 1". Also mentioned the absolute count (that would be) after the operation.

Reviewed-on: #3054
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-11-20 00:27:30 -08:00
1b74fe4c54 chore: assert quantity is 1 if otherwise ignored in addItem (#3051)
Some checks failed
Build / build (push) Has been cancelled
Build Docker image / docker (push) Has been cancelled
Reviewed-on: #3051
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-11-20 00:27:13 -08:00
cca9d56d11 feat: Accolades & Council Chat Access Import Sample (#3050)
All checks were successful
Build / build (push) Successful in 2m9s
Build Docker image / docker (push) Successful in 2m58s
Thought this one may fit better to import section, rather than account cheats.

Staff is intentionally set to false due to the lvl 5K limit in simulacrum, but the user can always easily set it to true, if they wish.

Reviewed-on: #3050
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-11-19 04:03:01 -08:00
a33d1199c7 chore: update licence (#3049)
Some checks failed
Build / build (push) Has been cancelled
Build Docker image / docker (push) Has been cancelled
- Switched to AGPL so "network use" (providing a hosted version of SpaceNinjaServer) requires the source code of it to be available (of course, unmodified instances needn't worry about this).
- Added the "commons clause" condition, which prohibits commercial use.

Reviewed-on: #3049
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-11-19 04:02:50 -08:00
4353f2915a chore: ask mongoose if import did anything (#3047)
Some checks failed
Build / build (push) Has been cancelled
Build Docker image / docker (push) Has been cancelled
Partially reverting 71785fffd7 for a much simpler solution.

Reviewed-on: #3047
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-11-19 04:02:43 -08:00
4b43825b67 chore: add Created to inventory schema (#3046)
All checks were successful
Build / build (push) Successful in 2m7s
Build Docker image / docker (push) Successful in 3m38s
Reviewed-on: #3046
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-11-18 05:07:21 -08:00
974636b5f5 chore(webui): give feedback when import errored (#3044)
Some checks failed
Build / build (push) Has been cancelled
Build Docker image / docker (push) Has been cancelled
Closes #3043

Reviewed-on: #3044
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-11-18 05:07:16 -08:00
e07f3bb5d2 chore: derive MatchmakingBuildId from buildLabel (#3041)
All checks were successful
Build / build (push) Successful in 1m30s
Build Docker image / docker (push) Successful in 2m53s
Reviewed-on: #3041
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-11-18 04:14:37 -08:00
cbd3750125 chore: update package-lock.json
Some checks failed
Build / build (push) Has been cancelled
Build Docker image / docker (push) Has been cancelled
2025-11-18 13:15:36 +01:00
78373c134c feat: mods in pre-U18.18 builds + equipment features in pre-U24.4 builds (#3040)
All checks were successful
Build / build (push) Successful in 1m15s
Build Docker image / docker (push) Successful in 2m2s
This implements support for mods in pre-U18.18  (The Silver Grove) builds, as well as support for Orokin Catalysts/Reactors, Forma, Exilus Adapters, and gilding modular weapons in pre-U24.4 builds. And should also fix #3036.

- Pre-Endo fusion system (consuming Fusion Cores and other mods to rank up the target), handles ranked Fusion Core mission drops.
- Attaching/detaching mods in U7-U8.
- Transmutation works but the game crashes if it rolls a mod that doesn't exist in that build.
- Riven mods work in U19 builds when added through the WebUI, but I can't get veiled Rivens to appear in the inventory in old builds yet (they probably changed the item paths when they made them stackable) to test if activating/unveiling them works, but rerolling them works.

This is a decently large PR that should be tested for regressions before merging, I have done a lot of thorough testing myself as I've worked on it, but I may have missed an edge-case somewhere.
And if there is anything I can improve code-wise in all these changes please reach out.

Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Reviewed-on: #3040
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: VoltPrime <subsonicjackal@gmail.com>
Co-committed-by: VoltPrime <subsonicjackal@gmail.com>
2025-11-18 00:27:07 -08:00
2bdf8b80fb chore: stats view U17.8 (#3039)
Some checks failed
Build / build (push) Has been cancelled
Build Docker image / docker (push) Has been cancelled
for some reason it uses post for that

Reviewed-on: #3039
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-11-18 00:26:50 -08:00
25d2b1c3ce chore(webui): don't display rank of mods with a max rank of 0 (#3037)
All checks were successful
Build / build (push) Successful in 2m8s
Build Docker image / docker (push) Successful in 3m2s
Makes the mods tab look a little cleaner with Parazon mods and Legendary Cores.

Reviewed-on: #3037
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: VoltPrime <subsonicjackal@gmail.com>
Co-committed-by: VoltPrime <subsonicjackal@gmail.com>
2025-11-18 00:07:47 -08:00
34ba56862b chore: remove tsturnonagain
All checks were successful
Build / build (push) Successful in 1m10s
Build Docker image / docker (push) Successful in 2m52s
2025-11-17 14:23:34 +01:00
de77c06120 feat: skipAllPopups cheat & update skipAllDialogue (#3035)
All checks were successful
Build / build (push) Successful in 2m7s
Build Docker image / docker (push) Successful in 2m13s
Reviewed-on: #3035
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-11-17 00:05:05 -08:00
b09ef0f21b feat(webui): revive booster (#3018)
Some checks failed
Build / build (push) Has been cancelled
Build Docker image / docker (push) Has been cancelled
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Reviewed-on: #3018
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-11-17 00:04:53 -08:00
43 changed files with 1116 additions and 445 deletions

151
LICENSE
View File

@@ -1,5 +1,15 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
“Commons Clause” License Condition v1.0
The Software is provided to you by the Licensor under the License, as defined below, subject to the following condition.
Without limiting other conditions in the License, the grant of rights under the License will not include, and the License does not grant to you, the right to Sell the Software.
For purposes of the foregoing, “Sell” means practicing any or all of the rights granted to you under the License to provide to third parties, for a fee or other consideration (including without limitation fees for hosting or consulting/ support services related to the Software), a product or service whose value derives, entirely or substantially, from the functionality of the Software. Any license notice or attribution required by the License must also include this Commons Clause License Condition notice.
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
@@ -7,17 +17,15 @@ GNU GENERAL PUBLIC LICENSE
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
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,
the GNU General Public License is intended to guarantee your freedom to
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. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
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
@@ -26,44 +34,34 @@ 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.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
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.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
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.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
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.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
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.
@@ -72,7 +70,7 @@ modification follow.
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"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.
@@ -549,35 +547,45 @@ 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. Use with the GNU Affero General Public License.
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 Affero General Public License into a single
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 special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
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 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
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 General
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 General Public License, you may choose any version ever published
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 General Public License can be used, that proxy's
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.
@@ -635,40 +643,29 @@ the "copyright" line and a pointer to where the full notice is found.
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 General Public License as published by
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 General Public License for more details.
GNU Affero General Public License for more details.
You should have received a copy of the GNU General Public License
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 the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
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 GPL, see
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

14
package-lock.json generated
View File

@@ -1,13 +1,12 @@
{
"name": "wf-emulator",
"name": "spaceninjaserver",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wf-emulator",
"name": "spaceninjaserver",
"version": "0.1.0",
"license": "GNU",
"dependencies": {
"body-parser": "^2.2.0",
"chokidar": "^4.0.3",
@@ -552,6 +551,7 @@
"integrity": "sha512-gTtSdWX9xiMPA/7MV9STjJOOYtWwIJIYxkQxnSV1U3xcE+mnJSH3f6zI0RYP+ew66WSlZ5ed+h0VCxsvdC1jJg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.41.0",
"@typescript-eslint/types": "8.41.0",
@@ -1180,6 +1180,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -2057,6 +2058,7 @@
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@@ -2223,6 +2225,7 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@@ -4362,6 +4365,7 @@
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -5203,6 +5207,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -5421,6 +5426,7 @@
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -5480,6 +5486,7 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"dependencies": {
"napi-postinstall": "^0.3.0"
},
@@ -5675,6 +5682,7 @@
"resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz",
"integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@colors/colors": "^1.6.0",
"@dabh/diagnostics": "^2.0.2",

View File

@@ -1,7 +1,7 @@
{
"name": "wf-emulator",
"name": "spaceninjaserver",
"version": "0.1.0",
"description": "WF Emulator",
"description": "SpaceNinjaServer",
"main": "index.ts",
"scripts": {
"start": "node --enable-source-maps build/src/index.js",
@@ -23,7 +23,6 @@
"update-translations": "cd scripts && node update-translations.cjs",
"fix": "npm run update-translations && npm run prettier"
},
"license": "GNU",
"type": "module",
"dependencies": {
"body-parser": "^2.2.0",

View File

@@ -1,4 +1,4 @@
import { toOid } from "../../helpers/inventoryHelpers.ts";
import { toOid2 } from "../../helpers/inventoryHelpers.ts";
import {
createVeiledRivenFingerprint,
createUnveiledRivenFingerprint,
@@ -6,13 +6,14 @@ import {
} from "../../helpers/rivenHelper.ts";
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
import { addMods, getInventory } from "../../services/inventoryService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { getAccountForRequest } from "../../services/loginService.ts";
import { getRandomElement } from "../../services/rngService.ts";
import type { RequestHandler } from "express";
import { ExportUpgrades } from "warframe-public-export-plus";
export const activateRandomModController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const account = await getAccountForRequest(req);
const accountId = account._id.toString();
const inventory = await getInventory(accountId, "RawUpgrades Upgrades instantFinishRivenChallenge");
const request = getJSONfromString<IActiveRandomModRequest>(String(req.body));
addMods(inventory, [
@@ -36,7 +37,7 @@ export const activateRandomModController: RequestHandler = async (req, res) => {
NewMod: {
UpgradeFingerprint: fingerprint,
ItemType: rivenType,
ItemId: toOid(inventory.Upgrades[upgradeIndex]._id)
ItemId: toOid2(inventory.Upgrades[upgradeIndex]._id, account.BuildLabel)
}
});
};

View File

@@ -1,7 +1,7 @@
import { fromOid, toOid } from "../../helpers/inventoryHelpers.ts";
import { fromOid, toOid2 } from "../../helpers/inventoryHelpers.ts";
import { createVeiledRivenFingerprint, rivenRawToRealWeighted } from "../../helpers/rivenHelper.ts";
import { addMiscItems, addMods, getInventory } from "../../services/inventoryService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { getAccountForRequest } from "../../services/loginService.ts";
import { getRandomElement, getRandomWeightedReward, getRandomWeightedRewardUc } from "../../services/rngService.ts";
import type { IUpgradeFromClient } from "../../types/inventoryTypes/inventoryTypes.ts";
import type { RequestHandler } from "express";
@@ -9,12 +9,15 @@ import type { TRarity } from "warframe-public-export-plus";
import { ExportBoosterPacks, ExportUpgrades } from "warframe-public-export-plus";
export const artifactTransmutationController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const account = await getAccountForRequest(req);
const accountId = account._id.toString();
const inventory = await getInventory(accountId);
const payload = JSON.parse(String(req.body)) as IArtifactTransmutationRequest;
inventory.RegularCredits -= payload.Cost;
inventory.FusionPoints -= payload.FusionPointCost;
if (payload.FusionPointCost) {
inventory.FusionPoints -= payload.FusionPointCost;
}
if (payload.RivenTransmute) {
addMiscItems(inventory, [
@@ -41,7 +44,7 @@ export const artifactTransmutationController: RequestHandler = async (req, res)
res.json({
NewMods: [
{
ItemId: toOid(inventory.Upgrades[upgradeIndex]._id),
ItemId: toOid2(inventory.Upgrades[upgradeIndex]._id, account.BuildLabel),
ItemType: rivenType,
UpgradeFingerprint: fingerprint
}
@@ -56,9 +59,10 @@ export const artifactTransmutationController: RequestHandler = async (req, res)
};
let forcedPolarity: string | undefined;
payload.Consumed.forEach(upgrade => {
upgrade.ItemCount ??= 1;
const meta = ExportUpgrades[upgrade.ItemType];
counts[meta.rarity] += upgrade.ItemCount;
if (fromOid(upgrade.ItemId) != "000000000000000000000000") {
if (fromOid(upgrade.ItemId) != "" && fromOid(upgrade.ItemId) != "000000000000000000000000") {
inventory.Upgrades.pull({ _id: fromOid(upgrade.ItemId) });
} else {
addMods(inventory, [
@@ -133,7 +137,7 @@ interface IArtifactTransmutationRequest {
LevelDiff: number;
Consumed: IUpgradeFromClient[];
Cost: number;
FusionPointCost: number;
FusionPointCost?: number;
RivenTransmute?: boolean;
}

View File

@@ -1,63 +1,126 @@
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { getAccountForRequest } from "../../services/loginService.ts";
import type { RequestHandler } from "express";
import type { IInventoryClient, IUpgradeClient } from "../../types/inventoryTypes/inventoryTypes.ts";
import type {
IInventoryClient,
IUpgradeClient,
IUpgradeFromClient
} from "../../types/inventoryTypes/inventoryTypes.ts";
import { addMods, getInventory } from "../../services/inventoryService.ts";
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
import { fromOid, version_compare } from "../../helpers/inventoryHelpers.ts";
export const artifactsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const account = await getAccountForRequest(req);
const accountId = account._id.toString();
const artifactsData = getJSONfromString<IArtifactsRequest>(String(req.body));
const { Upgrade, LevelDiff, Cost, FusionPointCost } = artifactsData;
const { Upgrade, LevelDiff, Cost, FusionPointCost, Consumed, Fingerprint } = artifactsData;
const inventory = await getInventory(accountId);
const { Upgrades } = inventory;
const { ItemType, UpgradeFingerprint, ItemId } = Upgrade;
const safeUpgradeFingerprint = UpgradeFingerprint || '{"lvl":0}';
const parsedUpgradeFingerprint = JSON.parse(safeUpgradeFingerprint) as { lvl: number };
parsedUpgradeFingerprint.lvl += LevelDiff;
const stringifiedUpgradeFingerprint = JSON.stringify(parsedUpgradeFingerprint);
if (!account.BuildLabel || version_compare(account.BuildLabel, "2016.08.19.17.12") >= 0) {
const safeUpgradeFingerprint = UpgradeFingerprint || '{"lvl":0}';
const parsedUpgradeFingerprint = JSON.parse(safeUpgradeFingerprint) as { lvl: number };
parsedUpgradeFingerprint.lvl += LevelDiff;
const stringifiedUpgradeFingerprint = JSON.stringify(parsedUpgradeFingerprint);
let itemIndex = Upgrades.findIndex(upgrade => upgrade._id.equals(ItemId.$oid));
let itemIndex = Upgrades.findIndex(upgrade => upgrade._id.equals(fromOid(ItemId)));
if (itemIndex !== -1) {
Upgrades[itemIndex].UpgradeFingerprint = stringifiedUpgradeFingerprint;
if (itemIndex !== -1) {
Upgrades[itemIndex].UpgradeFingerprint = stringifiedUpgradeFingerprint;
} else {
itemIndex =
Upgrades.push({
UpgradeFingerprint: stringifiedUpgradeFingerprint,
ItemType
}) - 1;
addMods(inventory, [{ ItemType, ItemCount: -1 }]);
}
if (!inventory.infiniteCredits) {
inventory.RegularCredits -= Cost;
}
if (!inventory.infiniteEndo) {
inventory.FusionPoints -= FusionPointCost;
}
if (artifactsData.LegendaryFusion) {
addMods(inventory, [
{
ItemType: "/Lotus/Upgrades/Mods/Fusers/LegendaryModFuser",
ItemCount: -1
}
]);
}
const changedInventory = (await inventory.save()).toJSON<IInventoryClient>();
const itemId =
changedInventory.Upgrades[itemIndex].ItemId.$oid ?? changedInventory.Upgrades[itemIndex].ItemId.$id;
if (!itemId) {
throw new Error("Item Id not found in upgradeMod");
}
res.send(itemId);
} else {
itemIndex =
Upgrades.push({
UpgradeFingerprint: stringifiedUpgradeFingerprint,
ItemType
}) - 1;
// Pre-U18.18.0 uses the old pre-Endo fusion system which uses a different UpgradeFingerprint format
// that has to be converted and consumes upgrades in the fusion proccess
const safeUpgradeFingerprint = `{"lvl":${Fingerprint?.substring(4, Fingerprint.lastIndexOf("|"))}}`;
const parsedUpgradeFingerprint = JSON.parse(safeUpgradeFingerprint) as { lvl: number };
if (LevelDiff) {
parsedUpgradeFingerprint.lvl += LevelDiff;
}
const stringifiedUpgradeFingerprint = JSON.stringify(parsedUpgradeFingerprint);
addMods(inventory, [{ ItemType, ItemCount: -1 }]);
}
let itemIndex = Upgrades.findIndex(upgrade => upgrade._id.equals(ItemId.$id));
if (!inventory.infiniteCredits) {
inventory.RegularCredits -= Cost;
}
if (!inventory.infiniteEndo) {
inventory.FusionPoints -= FusionPointCost;
}
if (itemIndex !== -1) {
Upgrades[itemIndex].UpgradeFingerprint = stringifiedUpgradeFingerprint;
} else {
itemIndex =
Upgrades.push({
UpgradeFingerprint: stringifiedUpgradeFingerprint,
ItemType
}) - 1;
if (artifactsData.LegendaryFusion) {
addMods(inventory, [
{
ItemType: "/Lotus/Upgrades/Mods/Fusers/LegendaryModFuser",
ItemCount: -1
addMods(inventory, [{ ItemType, ItemCount: -1 }]);
}
const itemId = Upgrades[itemIndex]._id.toString();
if (!itemId) {
throw new Error("Item Id not found in upgradeMod");
}
if (!inventory.infiniteCredits) {
inventory.RegularCredits -= Cost;
}
if (Consumed && Consumed.length > 0) {
for (const upgrade of Consumed) {
// The client does not send the expected information about the mods, so we have to check if it's an Upgrade or RawUpgrade manually.
if (Upgrades.id(fromOid(upgrade.ItemId))) {
Upgrades.pull({ _id: upgrade.ItemId.$id });
} else {
addMods(inventory, [
{
ItemType: upgrade.ItemType,
ItemCount: -1
}
]);
}
}
]);
itemIndex = Upgrades.findIndex(upgrade => upgrade._id.equals(itemId));
}
await inventory.save();
res.send(itemId);
}
const changedInventory = await inventory.save();
const itemId = changedInventory.toJSON<IInventoryClient>().Upgrades[itemIndex].ItemId.$oid;
if (!itemId) {
throw new Error("Item Id not found in upgradeMod");
}
res.send(itemId);
broadcastInventoryUpdate(req);
};
@@ -67,4 +130,6 @@ interface IArtifactsRequest {
Cost: number;
FusionPointCost: number;
LegendaryFusion?: boolean;
Fingerprint?: string;
Consumed?: IUpgradeFromClient[];
}

View File

@@ -262,9 +262,9 @@ const claimCompletedRecipe = async (
"",
"",
"",
umbraModA.ItemId.$oid,
umbraModB.ItemId.$oid,
umbraModC.ItemId.$oid
fromOid(umbraModA.ItemId),
fromOid(umbraModB.ItemId),
fromOid(umbraModC.ItemId)
]
}
],
@@ -284,7 +284,18 @@ const claimCompletedRecipe = async (
"/Lotus/Weapons/Tenno/Melee/Swords/UmbraKatana/UmbraKatana",
{
Configs: [
{ Upgrades: ["", "", "", "", "", "", sacrificeModA.ItemId.$oid, sacrificeModB.ItemId.$oid] }
{
Upgrades: [
"",
"",
"",
"",
"",
"",
fromOid(sacrificeModA.ItemId),
fromOid(sacrificeModB.ItemId)
]
}
],
XP: 450_000,
Features: EquipmentFeatures.DOUBLE_CAPACITY

View File

@@ -4,8 +4,9 @@ import type { TInventoryDatabaseDocument } from "../../models/inventoryModels/in
import { Inventory } from "../../models/inventoryModels/inventoryModel.ts";
import { config } from "../../services/configService.ts";
import allDialogue from "../../../static/fixed_responses/allDialogue.json" with { type: "json" };
import allPopups from "../../../static/fixed_responses/allPopups.json" with { type: "json" };
import type { ILoadoutDatabase } from "../../types/saveLoadoutTypes.ts";
import type { IInventoryClient, IShipInventory } from "../../types/inventoryTypes/inventoryTypes.ts";
import type { IInventoryClient, IShipInventory, IUpgradeClient } from "../../types/inventoryTypes/inventoryTypes.ts";
import { equipmentKeys } from "../../types/inventoryTypes/inventoryTypes.ts";
import type { IPolarity } from "../../types/inventoryTypes/commonInventoryTypes.ts";
import { ArtifactPolarity } from "../../types/inventoryTypes/commonInventoryTypes.ts";
@@ -29,7 +30,7 @@ import { getNemesisManifest } from "../../helpers/nemesisHelpers.ts";
import { getPersonalRooms } from "../../services/personalRoomsService.ts";
import type { IPersonalRoomsClient } from "../../types/personalRoomsTypes.ts";
import { Ship } from "../../models/shipModel.ts";
import { toLegacyOid, toOid, version_compare } from "../../helpers/inventoryHelpers.ts";
import { toLegacyOid, toOid, toOid2, version_compare } from "../../helpers/inventoryHelpers.ts";
import { Inbox } from "../../models/inboxModel.ts";
import { unixTimesInMs } from "../../constants/timeConstants.ts";
import { DailyDeal } from "../../models/worldStateModel.ts";
@@ -326,6 +327,12 @@ export const getInventoryResponse = async (
}
}
if (inventory.skipAllPopups) {
for (const str of allPopups) {
addString(inventoryResponse.NodeIntrosCompleted, str);
}
}
if (config.worldState?.baroTennoConRelay) {
[
"/Lotus/Types/Items/Events/TennoConRelay2022EarlyAccess",
@@ -447,28 +454,140 @@ export const getInventoryResponse = async (
inventoryResponse.Nemesis = undefined;
}
if (version_compare(buildLabel, "2018.02.22.14.34") < 0) {
const personalRoomsDb = await getPersonalRooms(inventory.accountOwnerId.toString());
const personalRooms = personalRoomsDb.toJSON<IPersonalRoomsClient>();
inventoryResponse.Ship = personalRooms.Ship;
if (version_compare(buildLabel, "2019.03.07.20.21") < 0) {
// Builds before U24.4.0 handle equipment features differently
for (const category of equipmentKeys) {
for (const item of inventoryResponse[category]) {
if (item.Features && item.Features & EquipmentFeatures.DOUBLE_CAPACITY) {
item.UnlockLevel = 1;
}
if (item.Features && item.Features & EquipmentFeatures.UTILITY_SLOT) {
item.UtilityUnlocked = 1;
}
if (item.Features && item.Features & EquipmentFeatures.GILDED) {
item.Gild = true;
}
}
}
if (version_compare(buildLabel, "2016.12.21.19.13") <= 0) {
// U19.5 and below use $id instead of $oid
for (const category of equipmentKeys) {
for (const item of inventoryResponse[category]) {
toLegacyOid(item.ItemId);
if (version_compare(buildLabel, "2018.02.22.14.34") < 0) {
const personalRoomsDb = await getPersonalRooms(inventory.accountOwnerId.toString());
const personalRooms = personalRoomsDb.toJSON<IPersonalRoomsClient>();
inventoryResponse.Ship = personalRooms.Ship;
if (version_compare(buildLabel, "2016.12.21.19.13") <= 0) {
// U19.5 and below use $id instead of $oid
for (const category of equipmentKeys) {
for (const item of inventoryResponse[category]) {
toLegacyOid(item.ItemId);
}
}
}
for (const upgrade of inventoryResponse.Upgrades) {
toLegacyOid(upgrade.ItemId);
}
if (inventoryResponse.BrandedSuits) {
for (const id of inventoryResponse.BrandedSuits) {
toLegacyOid(id);
if (version_compare(buildLabel, "2014.02.05.00.00") < 0) {
// Pre-U12 builds store mods in an array called Cards, and have no concept of RawUpgrades
inventoryResponse.Cards = [];
for (const rawUpgrade of inventoryResponse.RawUpgrades) {
const id = inventory.RawUpgrades.find(x => x.ItemType == rawUpgrade.ItemType)?._id;
if (id) {
for (let i = 0; i < rawUpgrade.ItemCount; i++) {
const card = {
ItemType: rawUpgrade.ItemType,
ItemId: toOid2(id, buildLabel),
Rank: 0,
AmountRemaining: rawUpgrade.ItemCount
} as IUpgradeClient;
// Client doesn't see the mods unless they are in both Cards and Upgrades
inventoryResponse.Cards.push(card);
inventoryResponse.Upgrades.push(card);
}
}
}
inventoryResponse.RawUpgrades = [];
for (const category of equipmentKeys) {
for (const item of inventoryResponse[category]) {
for (const config of item.Configs) {
if (config.Upgrades) {
// Convert installed upgrades for U10-U11
const convertedUpgrades: { $id: string }[] = [];
config.Upgrades.forEach(upgrade => {
const upgradeId = upgrade as string;
convertedUpgrades.push({ $id: upgradeId });
});
config.Upgrades = convertedUpgrades;
}
}
}
}
}
for (const upgrade of inventoryResponse.Upgrades) {
toLegacyOid(upgrade.ItemId);
if (version_compare(buildLabel, "2016.08.19.17.12") < 0) {
// Pre-U18.18 builds use a different UpgradeFingerprint format
let rank: number = 0;
if (upgrade.UpgradeFingerprint) {
rank = Number.parseFloat(
upgrade.UpgradeFingerprint.substring(
upgrade.UpgradeFingerprint.indexOf(":") + 1,
upgrade.UpgradeFingerprint.lastIndexOf("}")
)
);
}
upgrade.UpgradeFingerprint = `lvl=${rank}|`;
if (version_compare(buildLabel, "2014.04.10.17.47") < 0) {
// Pre-U10 builds
if (
!upgrade.AmountRemaining ||
(upgrade.AmountRemaining && upgrade.AmountRemaining <= 0)
) {
upgrade.AmountRemaining = 1;
}
upgrade.Rank = rank;
if (inventoryResponse.Cards) {
inventoryResponse.Cards.push(upgrade);
}
}
}
}
if (version_compare(buildLabel, "2014.02.05.00.00") < 0) {
// Convert installed mods for pre-U12 builds
for (const category of equipmentKeys) {
for (const item of inventoryResponse[category]) {
for (const config of item.Configs) {
if (config.Upgrades) {
for (let i = 0; i < config.Upgrades.length; i++) {
const id = config.Upgrades[i] as { $id: string | undefined };
const invUpgrade = inventoryResponse.Upgrades.find(
x => x.ItemId.$id == id.$id
);
if (invUpgrade) {
if (id.$id?.startsWith("/Lotus")) {
// Pre-U12 builds have no concept of RawUpgrades, have to convert the db entry to the closest id of an unranked copy
id.$id = inventoryResponse.Upgrades.find(
x => x.ItemType == id.$id
)?.ItemId.$id;
}
// Pre-U10
invUpgrade.ParentId = item.ItemId;
invUpgrade.Slot = i + 1;
}
}
}
}
}
}
}
if (inventoryResponse.BrandedSuits) {
for (const id of inventoryResponse.BrandedSuits) {
toLegacyOid(id);
}
}
if (inventoryResponse.GuildId) {
toLegacyOid(inventoryResponse.GuildId);
}
}
if (inventoryResponse.GuildId) {
toLegacyOid(inventoryResponse.GuildId);
}
}
}

View File

@@ -161,7 +161,16 @@ const createLoginResponse = (
if (version_compare(buildLabel, "2022.09.06.19.24") >= 0) {
resp.CrossPlatformAllowed = account.CrossPlatformAllowed;
resp.HUB = `${myUrlBase}/api/`;
resp.MatchmakingBuildId = buildConfig.matchmakingBuildId;
// The MatchmakingBuildId is a 64-bit integer represented as a decimal string. On live, the value is seemingly random per build, but really any value that is different across builds should work.
const [year, month, day, hour, minute] = buildLabel.split(".").map(x => parseInt(x));
resp.MatchmakingBuildId = (
year * 1_00_00_00_00 +
month * 1_00_00_00 +
day * 1_00_00 +
hour * 1_00 +
minute
).toString();
}
if (version_compare(buildLabel, "2023.04.25.23.40") >= 0) {
if (version_compare(buildLabel, "2025.08.26.09.49") >= 0) {

View File

@@ -1,4 +1,4 @@
import { fromDbOid, toMongoDate, version_compare } from "../../helpers/inventoryHelpers.ts";
import { fromDbOid, fromOid, toMongoDate, version_compare } from "../../helpers/inventoryHelpers.ts";
import type { IKnifeResponse } from "../../helpers/nemesisHelpers.ts";
import {
antivirusMods,
@@ -21,7 +21,7 @@ import { Loadout } from "../../models/inventoryModels/loadoutModel.ts";
import { addMods, freeUpSlot, getInventory } from "../../services/inventoryService.ts";
import { getAccountForRequest } from "../../services/loginService.ts";
import { SRng } from "../../services/rngService.ts";
import type { IMongoDate, IOid } from "../../types/commonTypes.ts";
import type { IMongoDate, IOid, IOidWithLegacySupport } from "../../types/commonTypes.ts";
import type { IEquipmentClient } from "../../types/equipmentTypes.ts";
import type {
IInnateDamageFingerprint,
@@ -420,7 +420,7 @@ interface IKnife {
const consumeModCharge = (
response: IKnifeResponse,
inventory: TInventoryDatabaseDocument,
upgrade: { ItemId: IOid; ItemType: string },
upgrade: { ItemId: IOidWithLegacySupport; ItemType: string },
dataknifeUpgrades: string[]
): void => {
response.UpgradeIds ??= [];
@@ -429,13 +429,13 @@ const consumeModCharge = (
response.UpgradeNew ??= [];
response.HasKnife = true;
if (upgrade.ItemId.$oid != "000000000000000000000000") {
const dbUpgrade = inventory.Upgrades.id(upgrade.ItemId.$oid)!;
if (fromOid(upgrade.ItemId) != "000000000000000000000000") {
const dbUpgrade = inventory.Upgrades.id(fromOid(upgrade.ItemId))!;
const fingerprint = JSON.parse(dbUpgrade.UpgradeFingerprint!) as { lvl: number };
fingerprint.lvl += 1;
dbUpgrade.UpgradeFingerprint = JSON.stringify(fingerprint);
response.UpgradeIds.push(upgrade.ItemId.$oid);
response.UpgradeIds.push(fromOid(upgrade.ItemId));
response.UpgradeTypes.push(upgrade.ItemType);
response.UpgradeFingerprints.push(fingerprint);
response.UpgradeNew.push(false);

View File

@@ -1,14 +1,16 @@
import type { RequestHandler } from "express";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { getAccountForRequest } from "../../services/loginService.ts";
import { addMiscItems, getInventory } from "../../services/inventoryService.ts";
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
import type { RivenFingerprint } from "../../helpers/rivenHelper.ts";
import { createUnveiledRivenFingerprint, randomiseRivenStats } from "../../helpers/rivenHelper.ts";
import { ExportUpgrades } from "warframe-public-export-plus";
import type { IOid } from "../../types/commonTypes.ts";
import type { IOidWithLegacySupport } from "../../types/commonTypes.ts";
import { toObjectId, toOid2 } from "../../helpers/inventoryHelpers.ts";
export const rerollRandomModController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const account = await getAccountForRequest(req);
const accountId = account._id.toString();
const request = getJSONfromString<RerollRandomModRequest>(String(req.body));
if ("ItemIds" in request) {
const inventory = await getInventory(accountId, "Upgrades MiscItems");
@@ -40,7 +42,7 @@ export const rerollRandomModController: RequestHandler = async (req, res) => {
}
changes.push({
ItemId: { $oid: request.ItemIds[0] },
ItemId: toOid2(toObjectId(request.ItemIds[0]), account.BuildLabel),
UpgradeFingerprint: upgrade.UpgradeFingerprint,
PendingRerollFingerprint: upgrade.PendingRerollFingerprint
});
@@ -76,7 +78,7 @@ interface AwDangitRequest {
}
interface IChange {
ItemId: IOid;
ItemId: IOidWithLegacySupport;
UpgradeFingerprint?: string;
PendingRerollFingerprint?: string;
}

View File

@@ -1,18 +1,22 @@
import type { RequestHandler } from "express";
import type { ISaveLoadoutRequest } from "../../types/saveLoadoutTypes.ts";
import { handleInventoryItemConfigChange } from "../../services/saveLoadoutService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { getAccountForRequest } from "../../services/loginService.ts";
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
export const saveLoadoutController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const account = await getAccountForRequest(req);
const body: ISaveLoadoutRequest = getJSONfromString<ISaveLoadoutRequest>(String(req.body));
// console.log(util.inspect(body, { showHidden: false, depth: null, colors: true }));
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { UpgradeVer, ...equipmentChanges } = body;
const newLoadoutId = await handleInventoryItemConfigChange(equipmentChanges, accountId);
const newLoadoutId = await handleInventoryItemConfigChange(
equipmentChanges,
account._id.toString(),
account.BuildLabel
);
//send back new loadout id, if new loadout was added
if (newLoadoutId) {

View File

@@ -1,155 +1,288 @@
import type { RequestHandler } from "express";
import type { IUpgradesRequest } from "../../types/requestTypes.ts";
import { fromOid, version_compare } from "../../helpers/inventoryHelpers.ts";
import type { IUpgradesRequest, IUpgradesRequestLegacy } from "../../types/requestTypes.ts";
import type { ArtifactPolarity, IAbilityOverride } from "../../types/inventoryTypes/commonInventoryTypes.ts";
import type { IInventoryClient, IMiscItem } from "../../types/inventoryTypes/inventoryTypes.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { addMiscItems, addRecipes, getInventory, updateCurrency } from "../../services/inventoryService.ts";
import { getAccountForRequest } from "../../services/loginService.ts";
import { addMiscItems, addMods, addRecipes, getInventory, updateCurrency } from "../../services/inventoryService.ts";
import { getRecipeByResult } from "../../services/itemDataService.ts";
import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
import { addInfestedFoundryXP, applyCheatsToInfestedFoundry } from "../../services/infestedFoundryService.ts";
import { sendWsBroadcastTo } from "../../services/wsService.ts";
import type { IEquipmentDatabase } from "../../types/equipmentTypes.ts";
import { EquipmentFeatures } from "../../types/equipmentTypes.ts";
import { Types } from "mongoose";
export const upgradesController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const payload = JSON.parse(String(req.body)) as IUpgradesRequest;
const account = await getAccountForRequest(req);
const accountId = account._id.toString();
const inventory = await getInventory(accountId);
const inventoryChanges: IInventoryChanges = {};
for (const operation of payload.Operations) {
if (
operation.UpgradeRequirement == "/Lotus/Types/Items/MiscItems/ModSlotUnlocker" ||
operation.UpgradeRequirement == "/Lotus/Types/Items/MiscItems/CustomizationSlotUnlocker"
) {
updateCurrency(inventory, 10, true);
} else if (
operation.OperationType != "UOT_SWAP_POLARITY" &&
operation.OperationType != "UOT_ABILITY_OVERRIDE"
) {
if (!operation.UpgradeRequirement) {
throw new Error(`${operation.OperationType} operation should be free?`);
}
addMiscItems(inventory, [
{
ItemType: operation.UpgradeRequirement,
ItemCount: -1
} satisfies IMiscItem
]);
}
if (operation.OperationType == "UOT_ABILITY_OVERRIDE") {
console.assert(payload.ItemCategory == "Suits");
const suit = inventory.Suits.id(payload.ItemId.$oid)!;
let newAbilityOverride: IAbilityOverride | undefined;
let totalPercentagePointsConsumed = 0;
if (operation.UpgradeRequirement != "") {
newAbilityOverride = {
Ability: operation.UpgradeRequirement,
Index: operation.PolarizeSlot
};
const recipe = getRecipeByResult(operation.UpgradeRequirement)!;
for (const ingredient of recipe.ingredients) {
totalPercentagePointsConsumed += ingredient.ItemCount / 10;
if (!inventory.infiniteHelminthMaterials) {
inventory.InfestedFoundry!.Resources!.find(x => x.ItemType == ingredient.ItemType)!.Count -=
ingredient.ItemCount;
if (account.BuildLabel && version_compare(account.BuildLabel, "2019.03.07.20.21") < 0) {
// Builds before U24.4.0 have a different request format
const payload = JSON.parse(String(req.body)) as IUpgradesRequestLegacy;
const itemId = fromOid(payload.Weapon.ItemId);
if (itemId) {
if (payload.IsSwappingOperation === true) {
const item = inventory[payload.Category].id(itemId)!;
for (let i = 0; i != payload.PolarityRemap.length; ++i) {
// Can't really be selective here like the newer format, it pushes everything in a way that the comparison fails against...
setSlotPolarity(item, i, payload.PolarityRemap[i].Value);
}
} else {
if (payload.PolarizeReq) {
switch (payload.PolarizeReq) {
case "/Lotus/Types/Items/MiscItems/Forma":
case "/Lotus/Types/Items/MiscItems/FormaUmbra": {
const item = inventory[payload.Category].id(itemId)!;
item.XP = 0;
setSlotPolarity(item, payload.PolarizeSlot, payload.PolarizeValue);
item.Polarized ??= 0;
item.Polarized += 1;
sendWsBroadcastTo(accountId, { update_inventory: true });
break;
}
default:
throw new Error("Unsupported polarize item: " + payload.PolarizeReq);
}
addMiscItems(inventory, [
{
ItemType: payload.PolarizeReq,
ItemCount: -1
} satisfies IMiscItem
]);
}
}
for (const entry of operation.PolarityRemap) {
suit.Configs[entry.Slot] ??= {};
suit.Configs[entry.Slot].AbilityOverride = newAbilityOverride;
}
const recipeChanges = addInfestedFoundryXP(inventory.InfestedFoundry!, totalPercentagePointsConsumed * 8);
addRecipes(inventory, recipeChanges);
inventoryChanges.Recipes = recipeChanges;
inventoryChanges.InfestedFoundry = inventory.toJSON<IInventoryClient>().InfestedFoundry;
applyCheatsToInfestedFoundry(inventory, inventoryChanges.InfestedFoundry!);
} else
switch (operation.UpgradeRequirement) {
case "/Lotus/Types/Items/MiscItems/OrokinReactor":
case "/Lotus/Types/Items/MiscItems/OrokinCatalyst": {
const item = inventory[payload.ItemCategory].id(payload.ItemId.$oid)!;
item.Features ??= 0;
item.Features |= EquipmentFeatures.DOUBLE_CAPACITY;
break;
}
case "/Lotus/Types/Items/MiscItems/UtilityUnlocker":
case "/Lotus/Types/Items/MiscItems/WeaponUtilityUnlocker": {
const item = inventory[payload.ItemCategory].id(payload.ItemId.$oid)!;
item.Features ??= 0;
item.Features |= EquipmentFeatures.UTILITY_SLOT;
break;
}
case "/Lotus/Types/Items/MiscItems/HeavyWeaponCatalyst": {
console.assert(payload.ItemCategory == "SpaceGuns");
const item = inventory[payload.ItemCategory].id(payload.ItemId.$oid)!;
item.Features ??= 0;
item.Features |= EquipmentFeatures.GRAVIMAG_INSTALLED;
break;
}
case "/Lotus/Types/Items/MiscItems/WeaponPrimaryArcaneUnlocker":
case "/Lotus/Types/Items/MiscItems/WeaponSecondaryArcaneUnlocker":
case "/Lotus/Types/Items/MiscItems/WeaponMeleeArcaneUnlocker":
case "/Lotus/Types/Items/MiscItems/WeaponAmpArcaneUnlocker":
case "/Lotus/Types/Items/MiscItems/WeaponArchGunArcaneUnlocker": {
const item = inventory[payload.ItemCategory].id(payload.ItemId.$oid)!;
item.Features ??= 0;
if (operation.OperationType == "UOT_ARCANE_UNLOCK_1") {
item.Features |= EquipmentFeatures.SECOND_ARCANE_SLOT;
} else {
item.Features |= EquipmentFeatures.ARCANE_SLOT;
if (payload.UtilityReq) {
switch (payload.UtilityReq) {
case "/Lotus/Types/Items/MiscItems/UtilityUnlocker": {
const item = inventory[payload.Category].id(itemId)!;
item.Features ??= 0;
item.Features |= EquipmentFeatures.UTILITY_SLOT;
break;
}
default:
throw new Error("Unsupported utility item: " + payload.UtilityReq);
}
break;
addMiscItems(inventory, [
{
ItemType: payload.UtilityReq,
ItemCount: -1
} satisfies IMiscItem
]);
}
case "/Lotus/Types/Items/MiscItems/ValenceAdapter": {
const item = inventory[payload.ItemCategory].id(payload.ItemId.$oid)!;
item.Features ??= 0;
item.Features |= EquipmentFeatures.VALENCE_SWAP;
break;
if (payload.UpgradeReq) {
switch (payload.UpgradeReq) {
case "/Lotus/Types/Items/MiscItems/OrokinReactor":
case "/Lotus/Types/Items/MiscItems/OrokinCatalyst": {
const item = inventory[payload.Category].id(itemId)!;
item.Features ??= 0;
item.Features |= EquipmentFeatures.DOUBLE_CAPACITY;
break;
}
default:
throw new Error("Unsupported upgrade: " + payload.UpgradeReq);
}
addMiscItems(inventory, [
{
ItemType: payload.UpgradeReq,
ItemCount: -1
} satisfies IMiscItem
]);
}
case "/Lotus/Types/Items/MiscItems/Forma":
case "/Lotus/Types/Items/MiscItems/FormaUmbra":
case "/Lotus/Types/Items/MiscItems/FormaAura":
case "/Lotus/Types/Items/MiscItems/FormaStance": {
const item = inventory[payload.ItemCategory].id(payload.ItemId.$oid)!;
item.XP = 0;
setSlotPolarity(item, operation.PolarizeSlot, operation.PolarizeValue);
item.Polarized ??= 0;
item.Polarized += 1;
sendWsBroadcastTo(accountId, { update_inventory: true }); // webui may need to to re-add "max rank" button
break;
}
// Handle attaching/detaching mods in U7-U8
if (payload.UpgradesToAttach && payload.UpgradesToAttach.length > 0) {
const item = inventory[payload.Category].id(itemId)!;
if (!item.Configs[0]) {
item.Configs.push({ Upgrades: ["", "", "", "", "", "", "", "", "", "", ""] });
}
case "/Lotus/Types/Items/MiscItems/ModSlotUnlocker": {
const item = inventory[payload.ItemCategory].id(payload.ItemId.$oid)!;
item.ModSlotPurchases ??= 0;
item.ModSlotPurchases += 1;
break;
if (item.Configs[0].Upgrades && item.Configs[0].Upgrades.length < 11) {
item.Configs[0].Upgrades.length = 11;
}
case "/Lotus/Types/Items/MiscItems/CustomizationSlotUnlocker": {
const item = inventory[payload.ItemCategory].id(payload.ItemId.$oid)!;
item.CustomizationSlotPurchases ??= 0;
item.CustomizationSlotPurchases += 1;
break;
}
case "": {
console.assert(operation.OperationType == "UOT_SWAP_POLARITY");
const item = inventory[payload.ItemCategory].id(payload.ItemId.$oid)!;
for (let i = 0; i != operation.PolarityRemap.length; ++i) {
if (operation.PolarityRemap[i].Slot != i) {
setSlotPolarity(item, i, operation.PolarityRemap[i].Value);
payload.UpgradesToAttach.forEach(upgrade => {
if (item.Configs[0].Upgrades && upgrade.ItemId.$id && upgrade.Slot) {
const arr = item.Configs[0].Upgrades;
if (arr.indexOf(upgrade.ItemId.$id) != -1) {
// Handle swapping mod to a different slot
arr[arr.indexOf(upgrade.ItemId.$id)] = "";
}
// We need to convert RawUpgrade into Upgrade once it's attached
const rawUpgrade = inventory.RawUpgrades.id(upgrade.ItemId.$id);
if (rawUpgrade) {
const newId = new Types.ObjectId().toString();
arr[upgrade.Slot - 1] = newId;
addMods(inventory, [
{
ItemType: upgrade.ItemType,
ItemCount: -1
}
]);
inventory.Upgrades.push({
UpgradeFingerprint: `{"lvl":0}`,
ItemType: upgrade.ItemType,
_id: newId
});
} else {
arr[upgrade.Slot - 1] = upgrade.ItemId.$id;
}
}
break;
}
default:
throw new Error("Unsupported upgrade: " + operation.UpgradeRequirement);
});
}
if (payload.UpgradesToDetach && payload.UpgradesToDetach.length > 0) {
const item = inventory[payload.Category].id(itemId)!;
if (item.Configs[0].Upgrades && item.Configs[0].Upgrades.length < 11) {
item.Configs[0].Upgrades.length = 11;
}
payload.UpgradesToDetach.forEach(upgrade => {
if (item.Configs[0].Upgrades && upgrade.ItemId.$id) {
const arr = item.Configs[0].Upgrades;
arr[arr.indexOf(upgrade.ItemId.$id)] = "";
}
});
}
}
} else {
const payload = JSON.parse(String(req.body)) as IUpgradesRequest;
for (const operation of payload.Operations) {
if (
operation.UpgradeRequirement == "/Lotus/Types/Items/MiscItems/ModSlotUnlocker" ||
operation.UpgradeRequirement == "/Lotus/Types/Items/MiscItems/CustomizationSlotUnlocker"
) {
updateCurrency(inventory, 10, true);
} else if (
operation.OperationType != "UOT_SWAP_POLARITY" &&
operation.OperationType != "UOT_ABILITY_OVERRIDE"
) {
if (!operation.UpgradeRequirement) {
throw new Error(`${operation.OperationType} operation should be free?`);
}
addMiscItems(inventory, [
{
ItemType: operation.UpgradeRequirement,
ItemCount: -1
} satisfies IMiscItem
]);
}
if (operation.OperationType == "UOT_ABILITY_OVERRIDE") {
console.assert(payload.ItemCategory == "Suits");
const suit = inventory.Suits.id(payload.ItemId.$oid)!;
let newAbilityOverride: IAbilityOverride | undefined;
let totalPercentagePointsConsumed = 0;
if (operation.UpgradeRequirement != "") {
newAbilityOverride = {
Ability: operation.UpgradeRequirement,
Index: operation.PolarizeSlot
};
const recipe = getRecipeByResult(operation.UpgradeRequirement)!;
for (const ingredient of recipe.ingredients) {
totalPercentagePointsConsumed += ingredient.ItemCount / 10;
if (!inventory.infiniteHelminthMaterials) {
inventory.InfestedFoundry!.Resources!.find(x => x.ItemType == ingredient.ItemType)!.Count -=
ingredient.ItemCount;
}
}
}
for (const entry of operation.PolarityRemap) {
suit.Configs[entry.Slot] ??= {};
suit.Configs[entry.Slot].AbilityOverride = newAbilityOverride;
}
const recipeChanges = addInfestedFoundryXP(
inventory.InfestedFoundry!,
totalPercentagePointsConsumed * 8
);
addRecipes(inventory, recipeChanges);
inventoryChanges.Recipes = recipeChanges;
inventoryChanges.InfestedFoundry = inventory.toJSON<IInventoryClient>().InfestedFoundry;
applyCheatsToInfestedFoundry(inventory, inventoryChanges.InfestedFoundry!);
} else
switch (operation.UpgradeRequirement) {
case "/Lotus/Types/Items/MiscItems/OrokinReactor":
case "/Lotus/Types/Items/MiscItems/OrokinCatalyst": {
const item = inventory[payload.ItemCategory].id(payload.ItemId.$oid)!;
item.Features ??= 0;
item.Features |= EquipmentFeatures.DOUBLE_CAPACITY;
break;
}
case "/Lotus/Types/Items/MiscItems/UtilityUnlocker":
case "/Lotus/Types/Items/MiscItems/WeaponUtilityUnlocker": {
const item = inventory[payload.ItemCategory].id(payload.ItemId.$oid)!;
item.Features ??= 0;
item.Features |= EquipmentFeatures.UTILITY_SLOT;
break;
}
case "/Lotus/Types/Items/MiscItems/HeavyWeaponCatalyst": {
console.assert(payload.ItemCategory == "SpaceGuns");
const item = inventory[payload.ItemCategory].id(payload.ItemId.$oid)!;
item.Features ??= 0;
item.Features |= EquipmentFeatures.GRAVIMAG_INSTALLED;
break;
}
case "/Lotus/Types/Items/MiscItems/WeaponPrimaryArcaneUnlocker":
case "/Lotus/Types/Items/MiscItems/WeaponSecondaryArcaneUnlocker":
case "/Lotus/Types/Items/MiscItems/WeaponMeleeArcaneUnlocker":
case "/Lotus/Types/Items/MiscItems/WeaponAmpArcaneUnlocker":
case "/Lotus/Types/Items/MiscItems/WeaponArchGunArcaneUnlocker": {
const item = inventory[payload.ItemCategory].id(payload.ItemId.$oid)!;
item.Features ??= 0;
if (operation.OperationType == "UOT_ARCANE_UNLOCK_1") {
item.Features |= EquipmentFeatures.SECOND_ARCANE_SLOT;
} else {
item.Features |= EquipmentFeatures.ARCANE_SLOT;
}
break;
}
case "/Lotus/Types/Items/MiscItems/ValenceAdapter": {
const item = inventory[payload.ItemCategory].id(payload.ItemId.$oid)!;
item.Features ??= 0;
item.Features |= EquipmentFeatures.VALENCE_SWAP;
break;
}
case "/Lotus/Types/Items/MiscItems/Forma":
case "/Lotus/Types/Items/MiscItems/FormaUmbra":
case "/Lotus/Types/Items/MiscItems/FormaAura":
case "/Lotus/Types/Items/MiscItems/FormaStance": {
const item = inventory[payload.ItemCategory].id(payload.ItemId.$oid)!;
item.XP = 0;
setSlotPolarity(item, operation.PolarizeSlot, operation.PolarizeValue);
item.Polarized ??= 0;
item.Polarized += 1;
sendWsBroadcastTo(accountId, { update_inventory: true }); // webui may need to to re-add "max rank" button
break;
}
case "/Lotus/Types/Items/MiscItems/ModSlotUnlocker": {
const item = inventory[payload.ItemCategory].id(payload.ItemId.$oid)!;
item.ModSlotPurchases ??= 0;
item.ModSlotPurchases += 1;
break;
}
case "/Lotus/Types/Items/MiscItems/CustomizationSlotUnlocker": {
const item = inventory[payload.ItemCategory].id(payload.ItemId.$oid)!;
item.CustomizationSlotPurchases ??= 0;
item.CustomizationSlotPurchases += 1;
break;
}
case "": {
console.assert(operation.OperationType == "UOT_SWAP_POLARITY");
const item = inventory[payload.ItemCategory].id(payload.ItemId.$oid)!;
for (let i = 0; i != operation.PolarityRemap.length; ++i) {
if (operation.PolarityRemap[i].Slot != i) {
setSlotPolarity(item, i, operation.PolarityRemap[i].Value);
}
}
break;
}
default:
throw new Error("Unsupported upgrade: " + operation.UpgradeRequirement);
}
}
}
await inventory.save();
res.json({ InventoryChanges: inventoryChanges });

View File

@@ -13,33 +13,46 @@ export const importController: RequestHandler = async (req, res) => {
const request = req.body as IImportRequest;
let anyKnownKey = false;
try {
const inventory = await getInventory(accountId);
importInventory(inventory, request.inventory);
if (inventory.isModified()) {
anyKnownKey = true;
await inventory.save();
}
const inventory = await getInventory(accountId);
if (importInventory(inventory, request.inventory)) {
anyKnownKey = true;
await inventory.save();
if ("LoadOutPresets" in request.inventory && request.inventory.LoadOutPresets) {
const loadout = await getLoadout(accountId);
importLoadOutPresets(loadout, request.inventory.LoadOutPresets);
if (loadout.isModified()) {
anyKnownKey = true;
await loadout.save();
}
}
if (
request.inventory.Ship?.Rooms || // very old accounts may have Ship with { Features: [ ... ] }
"Apartment" in request.inventory ||
"TailorShop" in request.inventory
) {
const personalRooms = await getPersonalRooms(accountId);
importPersonalRooms(personalRooms, request.inventory);
if (personalRooms.isModified()) {
anyKnownKey = true;
await personalRooms.save();
}
}
if (!anyKnownKey) {
res.send("noKnownKey").end();
}
broadcastInventoryUpdate(req);
} catch (e) {
console.error(e);
res.send((e as Error).message);
}
if ("LoadOutPresets" in request.inventory && request.inventory.LoadOutPresets) {
anyKnownKey = true;
const loadout = await getLoadout(accountId);
importLoadOutPresets(loadout, request.inventory.LoadOutPresets);
await loadout.save();
}
if (
request.inventory.Ship?.Rooms || // very old accounts may have Ship with { Features: [ ... ] }
"Apartment" in request.inventory ||
"TailorShop" in request.inventory
) {
anyKnownKey = true;
const personalRooms = await getPersonalRooms(accountId);
importPersonalRooms(personalRooms, request.inventory);
await personalRooms.save();
}
res.json(anyKnownKey);
broadcastInventoryUpdate(req);
res.end();
};
interface IImportRequest {

View File

@@ -20,6 +20,10 @@ export const version_compare = (a: string, b: string): number => {
return 0;
};
export const toObjectId = (s: string): Types.ObjectId => {
return new Types.ObjectId(s);
};
export const toOid = (objectId: Types.ObjectId): IOid => {
return { $oid: objectId.toString() };
};

View File

@@ -1060,7 +1060,6 @@ const EquipmentSchema = new Schema<IEquipmentDatabase>(
InfestationDays: Number,
InfestationType: String,
ModularParts: { type: [String], default: undefined },
UnlockLevel: Number,
Expiry: Date,
SkillTree: String,
OffensiveUpgrade: String,
@@ -1448,6 +1447,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
// SNS account cheats
skipAllDialogue: Boolean,
skipAllPopups: Boolean,
dontSubtractPurchaseCreditCost: Boolean,
dontSubtractPurchasePlatinumCost: Boolean,
dontSubtractPurchaseItemCost: Boolean,
@@ -1492,6 +1492,8 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
relicRewardItemCountMultiplier: { type: Number, default: 1 },
nightwaveStandingMultiplier: { type: Number, default: 1 },
Created: Date,
SubscribedToEmails: { type: Number, default: 0 },
SubscribedToEmailsPersonalized: { type: Number, default: 0 },
RewardSeed: BigInt,

View File

@@ -9,6 +9,7 @@ statsRouter.get("/view.php", viewController);
statsRouter.get("/profileStats.php", viewController);
statsRouter.get("/leaderboard.php", leaderboardGetController);
statsRouter.post("/upload.php", uploadController);
statsRouter.post("/view.php", viewController);
statsRouter.post("/leaderboardWeekly.php", leaderboardPostController);
statsRouter.post("/leaderboardArchived.php", leaderboardPostController);

View File

@@ -5,13 +5,11 @@ import { repoDir } from "../helpers/pathHelper.ts";
interface IBuildConfig {
version: string;
buildLabel: string;
matchmakingBuildId: string;
}
export const buildConfig: IBuildConfig = {
version: "",
buildLabel: "",
matchmakingBuildId: ""
buildLabel: ""
};
const buildConfigPath = path.join(repoDir, "static/data/buildConfig.json");

View File

@@ -254,21 +254,17 @@ const convertItemConfig = <T extends IItemConfig>(client: T): T => {
};
};
export const importInventory = (db: TInventoryDatabaseDocument, client: Partial<IInventoryClient>): boolean => {
let anyKnownKey = false;
export const importInventory = (db: TInventoryDatabaseDocument, client: Partial<IInventoryClient>): void => {
for (const key of equipmentKeys) {
if (client[key] !== undefined) {
anyKnownKey = true;
replaceArray<IEquipmentDatabase>(db[key], client[key].map(convertEquipment));
}
}
if (client.WeaponSkins !== undefined) {
anyKnownKey = true;
replaceArray<IWeaponSkinDatabase>(db.WeaponSkins, client.WeaponSkins.map(convertWeaponSkin));
}
for (const key of ["Upgrades", "CrewShipSalvagedWeaponSkins", "CrewShipWeaponSkins"] as const) {
if (client[key] !== undefined) {
anyKnownKey = true;
replaceArray<IUpgradeDatabase>(db[key], client[key].map(convertUpgrade));
}
}
@@ -284,7 +280,6 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial<
"CrewShipRawSalvage"
] as const) {
if (client[key] !== undefined) {
anyKnownKey = true;
db[key].splice(0, db[key].length);
client[key].forEach(x => {
db[key].push({
@@ -296,13 +291,11 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial<
}
for (const key of ["AdultOperatorLoadOuts", "OperatorLoadOuts", "KahlLoadOuts"] as const) {
if (client[key] !== undefined) {
anyKnownKey = true;
replaceArray<IOperatorConfigDatabase>(db[key], client[key].map(convertOperatorConfig));
}
}
for (const key of slotNames) {
if (client[key] !== undefined) {
anyKnownKey = true;
replaceSlots(db[key], client[key]);
}
}
@@ -319,7 +312,6 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial<
"Counselor"
] as const) {
if (client[key] !== undefined) {
anyKnownKey = true;
db[key] = client[key];
}
}
@@ -346,7 +338,6 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial<
"EchoesHexConquestCacheScoreMission"
] as const) {
if (client[key] !== undefined) {
anyKnownKey = true;
db[key] = client[key];
}
}
@@ -362,7 +353,6 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial<
"ActiveAvatarImageType"
] as const) {
if (client[key] !== undefined) {
anyKnownKey = true;
db[key] = client[key];
}
}
@@ -379,7 +369,6 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial<
"EchoesHexConquestActiveStickers"
] as const) {
if (client[key] !== undefined) {
anyKnownKey = true;
db[key] = client[key];
}
}
@@ -393,133 +382,103 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial<
"EntratiVaultCountResetDate"
] as const) {
if (client[key] !== undefined) {
anyKnownKey = true;
db[key] = fromMongoDate(client[key]);
}
}
// IRewardAtten[]
for (const key of ["SortieRewardAttenuation", "SpecialItemRewardAttenuation"] as const) {
if (client[key] !== undefined) {
anyKnownKey = true;
db[key] = client[key];
}
}
if (client.XPInfo !== undefined) {
anyKnownKey = true;
db.XPInfo = client.XPInfo;
}
if (client.CurrentLoadOutIds !== undefined) {
anyKnownKey = true;
db.CurrentLoadOutIds = client.CurrentLoadOutIds;
}
if (client.Affiliations !== undefined) {
anyKnownKey = true;
db.Affiliations = client.Affiliations;
}
if (client.FusionTreasures !== undefined) {
anyKnownKey = true;
db.FusionTreasures = client.FusionTreasures;
}
if (client.FocusUpgrades !== undefined) {
anyKnownKey = true;
db.FocusUpgrades = client.FocusUpgrades;
}
if (client.EvolutionProgress !== undefined) {
anyKnownKey = true;
db.EvolutionProgress = client.EvolutionProgress;
}
if (client.InfestedFoundry !== undefined) {
anyKnownKey = true;
db.InfestedFoundry = convertInfestedFoundry(client.InfestedFoundry);
}
if (client.DialogueHistory !== undefined) {
anyKnownKey = true;
db.DialogueHistory = convertDialogueHistory(client.DialogueHistory);
}
if (client.CustomMarkers !== undefined) {
anyKnownKey = true;
db.CustomMarkers = client.CustomMarkers;
}
if (client.ChallengeProgress !== undefined) {
anyKnownKey = true;
db.ChallengeProgress = client.ChallengeProgress;
}
if (client.QuestKeys !== undefined) {
anyKnownKey = true;
replaceArray<IQuestKeyDatabase>(db.QuestKeys, client.QuestKeys.map(convertQuestKey));
}
if (client.LastRegionPlayed !== undefined) {
anyKnownKey = true;
db.LastRegionPlayed = client.LastRegionPlayed;
}
if (client.PendingRecipes !== undefined) {
anyKnownKey = true;
replaceArray<IPendingRecipeDatabase>(db.PendingRecipes, client.PendingRecipes.map(convertPendingRecipe));
}
if (client.TauntHistory !== undefined) {
anyKnownKey = true;
db.TauntHistory = client.TauntHistory;
}
if (client.LoreFragmentScans !== undefined) {
anyKnownKey = true;
db.LoreFragmentScans = client.LoreFragmentScans;
}
for (const key of ["PendingSpectreLoadouts", "SpectreLoadouts"] as const) {
if (client[key] !== undefined) {
anyKnownKey = true;
db[key] = client[key];
}
}
if (client.FocusXP !== undefined) {
anyKnownKey = true;
db.FocusXP = client.FocusXP;
}
for (const key of ["Alignment", "AlignmentReplay"] as const) {
if (client[key] !== undefined) {
anyKnownKey = true;
db[key] = client[key];
}
}
if (client.StepSequencers !== undefined) {
anyKnownKey = true;
db.StepSequencers = client.StepSequencers;
}
if (client.CompletedJobChains !== undefined) {
anyKnownKey = true;
db.CompletedJobChains = client.CompletedJobChains;
}
if (client.Nemesis !== undefined) {
anyKnownKey = true;
db.Nemesis = convertNemesis(client.Nemesis);
}
if (client.PlayerSkills !== undefined) {
anyKnownKey = true;
db.PlayerSkills = client.PlayerSkills;
}
if (client.LotusCustomization !== undefined) {
anyKnownKey = true;
db.LotusCustomization = convertItemConfig(client.LotusCustomization);
}
if (client.CollectibleSeries !== undefined) {
anyKnownKey = true;
db.CollectibleSeries = client.CollectibleSeries;
}
for (const key of ["LibraryAvailableDailyTaskInfo", "LibraryActiveDailyTaskInfo"] as const) {
if (client[key] !== undefined) {
anyKnownKey = true;
db[key] = client[key];
}
}
if (client.SongChallenges !== undefined) {
anyKnownKey = true;
db.SongChallenges = client.SongChallenges;
}
if (client.Missions !== undefined) {
anyKnownKey = true;
db.Missions = client.Missions;
}
if (client.FlavourItems !== undefined) {
anyKnownKey = true;
db.FlavourItems.splice(0, db.FlavourItems.length);
client.FlavourItems.forEach(x => {
db.FlavourItems.push({
@@ -528,14 +487,11 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial<
});
}
if (client.Accolades !== undefined) {
anyKnownKey = true;
db.Accolades = client.Accolades;
}
if (client.Boosters !== undefined) {
anyKnownKey = true;
replaceArray<IBooster>(db.Boosters, client.Boosters);
}
return anyKnownKey;
};
export const importLoadOutConfig = (client: ILoadoutConfigClient): ILoadoutConfigDatabase => {

View File

@@ -33,7 +33,7 @@ import type { IGenericUpdate, IUpdateNodeIntrosResponse } from "../types/generic
import type { IKeyChainRequest, IMissionInventoryUpdateRequest } from "../types/requestTypes.ts";
import { logger } from "../utils/logger.ts";
import { convertInboxMessage, fromStoreItem, getKeyChainItems } from "./itemDataService.ts";
import type { IFlavourItem, IItemConfig } from "../types/inventoryTypes/commonInventoryTypes.ts";
import type { IFlavourItem, IItemConfig, IItemConfigDatabase } from "../types/inventoryTypes/commonInventoryTypes.ts";
import type { IDefaultUpgrade, IPowersuit, ISentinel, TStandingLimitBin } from "warframe-public-export-plus";
import {
ExportArcanes,
@@ -93,9 +93,11 @@ import type {
import { EquipmentFeatures, Status } from "../types/equipmentTypes.ts";
import type { ITypeCount } from "../types/commonTypes.ts";
import { skinLookupTable } from "../helpers/skinLookupTable.ts";
import type { TLoadoutDatabaseDocument } from "../models/inventoryModels/loadoutModel.ts";
export const createInventory = async (
accountOwnerId: Types.ObjectId,
loadout: TLoadoutDatabaseDocument,
defaultItemReferences: { loadOutPresetId: Types.ObjectId; ship: Types.ObjectId }
): Promise<void> => {
try {
@@ -115,10 +117,29 @@ export const createInventory = async (
if (config.skipTutorial) {
inventory.PlayedParkourTutorial = true;
await addStartingGear(inventory);
const startingGear = await addStartingGear(inventory);
await completeQuest(inventory, "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain", undefined);
await completeQuest(inventory, "/Lotus/Types/Keys/ModQuest/ModQuestKeyChain", undefined);
loadout.NORMAL.push({
s: {
ItemId: new Types.ObjectId(fromOid(startingGear.Suits![0].ItemId))
},
l: {
ItemId: new Types.ObjectId(fromOid(startingGear.LongGuns![0].ItemId))
},
p: {
ItemId: new Types.ObjectId(fromOid(startingGear.Pistols![0].ItemId))
},
m: {
ItemId: new Types.ObjectId(fromOid(startingGear.Melee![0].ItemId))
},
a: {
ItemId: new Types.ObjectId(fromOid(startingGear.SpecialItems![0].ItemId))
}
});
await loadout.save();
const completedMissions = ["SolNode27", "SolNode89", "SolNode63", "SolNode85", "SolNode15", "SolNode79"];
inventory.Missions.push(
@@ -377,6 +398,9 @@ export const addItem = async (
FusionTreasures: fusionTreasureChanges
};
} else if (ExportResources[typeName].productCategory == "Ships") {
if (quantity != 1) {
throw new Error(`unexpected acquisition quantity of Ships: got ${quantity}, expected 1`);
}
const oid = await createShip(inventory.accountOwnerId, typeName);
inventory.Ships.push(oid);
return {
@@ -388,6 +412,9 @@ export const addItem = async (
]
};
} else if (ExportResources[typeName].productCategory == "CrewShips") {
if (quantity != 1) {
throw new Error(`unexpected acquisition quantity of CrewShips: got ${quantity}, expected 1`);
}
return {
...(await addCrewShip(inventory, typeName)),
// fix to unlock railjack modding, item bellow supposed to be obtained from archwing quest
@@ -522,6 +549,11 @@ export const addItem = async (
if (typeName in ExportWeapons) {
const weapon = ExportWeapons[typeName];
if (weapon.totalDamage != 0) {
if (quantity != 1) {
throw new Error(
`unexpected acquisition quantity of ${weapon.productCategory}: got ${quantity}, expected 1`
);
}
const defaultOverwrites: Partial<IEquipmentDatabase> = {};
if (premiumPurchase) {
defaultOverwrites.Features = EquipmentFeatures.DOUBLE_CAPACITY;
@@ -578,6 +610,9 @@ export const addItem = async (
};
} else if (targetFingerprint) {
// Sister's Hound
if (quantity != 1) {
throw new Error(`unexpected acquisition quantity of MoaPets: got ${quantity}, expected 1`);
}
const targetFingerprintObj = JSON.parse(targetFingerprint) as INemesisPetTargetFingerprint;
const head = targetFingerprintObj.Parts[0];
const defaultOverwrites: Partial<IEquipmentDatabase> = {
@@ -653,6 +688,9 @@ export const addItem = async (
const key = ExportKeys[typeName];
if (key.chainStages) {
if (quantity != 1) {
throw new Error(`unexpected acquisition quantity of QuestKeys: got ${quantity}, expected 1`);
}
const key = addQuestKey(inventory, { ItemType: typeName });
if (!key) return {};
return { QuestKeys: [key] };
@@ -695,6 +733,9 @@ export const addItem = async (
if (typeName.endsWith("AugmentCard")) break;
switch (typeName.substring(1).split("/")[2]) {
default: {
if (quantity != 1) {
throw new Error(`unexpected acquisition quantity of Suits: got ${quantity}, expected 1`);
}
return {
...(await addPowerSuit(inventory, typeName, {
Features: premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined
@@ -703,6 +744,9 @@ export const addItem = async (
};
}
case "Archwing": {
if (quantity != 1) {
throw new Error(`unexpected acquisition quantity of SpaceSuits: got ${quantity}, expected 1`);
}
inventory.ArchwingEnabled = true;
return {
...addSpaceSuit(
@@ -715,6 +759,9 @@ export const addItem = async (
};
}
case "EntratiMech": {
if (quantity != 1) {
throw new Error(`unexpected acquisition quantity of MechSuits: got ${quantity}, expected 1`);
}
return {
...(await addMechSuit(
inventory,
@@ -747,16 +794,18 @@ export const addItem = async (
case "Boons":
// Can purchase /Lotus/Upgrades/Boons/DuviriVendorBoonItem from Acrithis, doesn't need to be added to inventory.
logger.debug(`acquisition of ${typeName} is not committed to inventory`);
return {};
case "Stickers":
{
const entry = inventory.RawUpgrades.find(x => x.ItemType == typeName);
if (entry && entry.ItemCount >= 10) {
logger.debug(`adding ${quantity} pix chip(s) instead of ${typeName}`);
const miscItemChanges = [
{
ItemType: "/Lotus/Types/Items/MiscItems/1999ConquestBucks",
ItemCount: 1
ItemCount: quantity
}
];
addMiscItems(inventory, miscItemChanges);
@@ -779,6 +828,9 @@ export const addItem = async (
break;
case "Skins": {
if (quantity != 1) {
throw new Error(`unexpected acquisition quantity of Skins: got ${quantity}, expected 1`);
}
return addSkin(inventory, typeName);
}
}
@@ -787,6 +839,9 @@ export const addItem = async (
case "Types":
switch (typeName.substring(1).split("/")[2]) {
case "Sentinels": {
if (quantity != 1) {
throw new Error(`unexpected acquisition quantity of Sentinels: got ${quantity}, expected 1`);
}
return addSentinel(inventory, typeName, premiumPurchase);
}
case "Game": {
@@ -813,9 +868,19 @@ export const addItem = async (
typeName != "/Lotus/Types/Game/KubrowPet/BlankTraitPrint" &&
typeName != "/Lotus/Types/Game/KubrowPet/ImprintedTraitPrint"
) {
if (quantity != 1) {
throw new Error(
`unexpected acquisition quantity of KubrowPet: got ${quantity}, expected 1`
);
}
return addKubrowPet(inventory, typeName, undefined, premiumPurchase);
}
} else if (typeName.startsWith("/Lotus/Types/Game/CrewShip/CrewMember/")) {
if (quantity != 1) {
throw new Error(
`unexpected acquisition quantity of CrewMember: got ${quantity}, expected 1`
);
}
if (!seed) {
throw new Error(`Expected crew member to have a seed`);
}
@@ -825,12 +890,22 @@ export const addItem = async (
...occupySlot(inventory, InventorySlot.CREWMEMBERS, premiumPurchase)
};
} else if (typeName == "/Lotus/Types/Game/CrewShip/RailJack/DefaultHarness") {
if (quantity != 1) {
throw new Error(
`unexpected acquisition quantity of CrewShipHarness: got ${quantity}, expected 1`
);
}
return addCrewShipHarness(inventory, typeName);
}
break;
}
case "Items": {
if (typeName.substring(1).split("/")[3] == "Emotes") {
if (quantity != 1) {
throw new Error(
`unexpected acquisition quantity of FlavourItems: got ${quantity}, expected 1`
);
}
return addCustomization(inventory, typeName);
}
break;
@@ -840,6 +915,9 @@ export const addItem = async (
logger.warn("refusing to add Horse because account already has one");
return {};
}
if (quantity != 1) {
throw new Error(`unexpected acquisition quantity of Horses: got ${quantity}, expected 1`);
}
const horseIndex = inventory.Horses.push({ ItemType: typeName });
return {
Horses: [inventory.Horses[horseIndex - 1].toJSON<IEquipmentClient>()]
@@ -847,11 +925,19 @@ export const addItem = async (
}
case "Vehicles":
if (typeName == "/Lotus/Types/Vehicles/Motorcycle/MotorcyclePowerSuit") {
if (quantity != 1) {
throw new Error(`unexpected acquisition quantity of Vehicles: got ${quantity}, expected 1`);
}
return addMotorcycle(inventory, typeName);
}
break;
case "Lore":
if (typeName == "/Lotus/Types/Lore/Fragments/GrineerGhoulFragments/GhoulFragmentRewards") {
if (quantity != 1) {
throw new Error(
`unexpected acquisition quantity of LoreFragmentScans: got ${quantity}, expected 1`
);
}
const fragmentType = getRandomElement([
"/Lotus/Types/Lore/Fragments/GrineerGhoulFragments/GhoulFragmentA",
"/Lotus/Types/Lore/Fragments/GrineerGhoulFragments/GhoulFragmentB",
@@ -885,6 +971,11 @@ export const addItem = async (
case "Pistols":
case "LongGuns":
case "Melee": {
if (quantity != 1) {
throw new Error(
`unexpected acquisition quantity of ${productCategory}: got ${quantity}, expected 1`
);
}
const inventoryChanges = addEquipment(inventory, productCategory, typeName);
return {
...inventoryChanges,
@@ -922,9 +1013,9 @@ export const addItems = async (
export const applyDefaultUpgrades = (
inventory: TInventoryDatabaseDocument,
defaultUpgrades: IDefaultUpgrade[] | undefined
): IItemConfig[] => {
): IItemConfigDatabase[] => {
const modsToGive: IRawUpgrade[] = [];
const configs: IItemConfig[] = [];
const configs: IItemConfigDatabase[] = [];
if (defaultUpgrades) {
const upgrades = [];
for (const defaultUpgrade of defaultUpgrades) {
@@ -1850,7 +1941,9 @@ export const addMiscItems = (inventory: TInventoryDatabaseDocument, itemsArray:
if (MiscItems[itemIndex].ItemCount == 0) {
MiscItems.splice(itemIndex, 1);
} else if (MiscItems[itemIndex].ItemCount < 0) {
throw new Error(`inventory.MiscItems has a negative count for ${ItemType} after subtracting ${ItemCount}`);
throw new Error(
`Cannot remove ${ItemCount * -1}x ${ItemType} from MiscItems, would be left with ${MiscItems[itemIndex].ItemCount}`
);
}
});
};
@@ -1873,7 +1966,7 @@ const applyArrayChanges = (
arr.splice(itemIndex, 1);
} else if (arr[itemIndex].ItemCount < 0) {
throw new Error(
`inventory.${key} has a negative count for ${change.ItemType} after subtracting ${change.ItemCount}`
`Cannot remove ${change.ItemCount * -1}x ${change.ItemType} from ${key}, would be left with ${arr[itemIndex].ItemCount}`
);
}
}
@@ -1922,7 +2015,7 @@ export const addMods = (inventory: TInventoryDatabaseDocument, itemsArray: IRawU
RawUpgrades.splice(itemIndex, 1);
} else if (RawUpgrades[itemIndex].ItemCount < 0) {
throw new Error(
`inventory.RawUpgrades has a negative count for ${ItemType} after subtracting ${ItemCount}`
`Cannot remove ${ItemCount * -1}x ${ItemType} from RawUpgrades, would be left with ${RawUpgrades[itemIndex].ItemCount}`
);
}
});
@@ -1939,7 +2032,7 @@ export const addFusionTreasures = (inventory: TInventoryDatabaseDocument, itemsA
FusionTreasures.splice(itemIndex, 1);
} else if (FusionTreasures[itemIndex].ItemCount <= 0) {
throw new Error(
`inventory.FusionTreasures has a negative count for ${ItemType} after subtracting ${ItemCount}`
`Cannot remove ${ItemCount * -1}x ${ItemType} from FusionTreasures, would be left with ${FusionTreasures[itemIndex].ItemCount}`
);
}
} else {

View File

@@ -3,7 +3,7 @@ import { createInventory } from "./inventoryService.ts";
import type { IDatabaseAccountJson, IDatabaseAccountRequiredFields } from "../types/loginTypes.ts";
import { createShip } from "./shipService.ts";
import type { Document, Types } from "mongoose";
import { Loadout } from "../models/inventoryModels/loadoutModel.ts";
import { Loadout, type TLoadoutDatabaseDocument } from "../models/inventoryModels/loadoutModel.ts";
import { PersonalRooms } from "../models/personalRoomsModel.ts";
import type { Request } from "express";
import { config } from "./configService.ts";
@@ -39,10 +39,10 @@ export const createAccount = async (accountData: IDatabaseAccountRequiredFields)
const account = new Account(accountData);
try {
await account.save();
const loadoutId = await createLoadout(account._id);
const loadout = await createLoadout(account._id);
const shipId = await createShip(account._id);
await createPersonalRooms(account._id, shipId);
await createInventory(account._id, { loadOutPresetId: loadoutId, ship: shipId });
await createInventory(account._id, loadout, { loadOutPresetId: loadout._id, ship: shipId });
await createStats(account._id.toString());
return account.toJSON();
} catch (error) {
@@ -53,10 +53,10 @@ export const createAccount = async (accountData: IDatabaseAccountRequiredFields)
}
};
export const createLoadout = async (accountId: Types.ObjectId): Promise<Types.ObjectId> => {
export const createLoadout = async (accountId: Types.ObjectId): Promise<TLoadoutDatabaseDocument> => {
const loadout = new Loadout({ loadoutOwnerId: accountId });
const savedLoadout = await loadout.save();
return savedLoadout._id;
return savedLoadout;
};
export const createPersonalRooms = async (accountId: Types.ObjectId, shipId: Types.ObjectId): Promise<void> => {

View File

@@ -84,7 +84,7 @@ import {
import { config } from "./configService.ts";
import libraryDailyTasks from "../../static/fixed_responses/libraryDailyTasks.json" with { type: "json" };
import type { IGoal, ISyndicateMissionInfo } from "../types/worldStateTypes.ts";
import { fromOid } from "../helpers/inventoryHelpers.ts";
import { fromOid, version_compare } from "../helpers/inventoryHelpers.ts";
import type { TAccountDocument } from "./loginService.ts";
import type { ITypeCount } from "../types/commonTypes.ts";
import type { IEquipmentClient } from "../types/equipmentTypes.ts";
@@ -480,9 +480,32 @@ export const addMissionInventoryUpdates = async (
case "Upgrades":
value.forEach(clientUpgrade => {
const id = fromOid(clientUpgrade.ItemId);
if (id == "") {
// Really old builds (tested U7-U8) do not have the UpgradeFingerprint set for unranked mod drops
clientUpgrade.UpgradeFingerprint ??= "lvl=0|";
// U11 and below also don't initialize ItemCount since RawUpgrade doesn't exist in them
clientUpgrade.ItemCount ??= 1;
if (account.BuildLabel && version_compare(account.BuildLabel, "2016.08.19.17.12") < 0) {
// Acquired Mods have a different UpgradeFingerprint format in pre-U18.18.0 builds, this converts them to the format the database expects
clientUpgrade.UpgradeFingerprint = `{"lvl":${clientUpgrade.UpgradeFingerprint.substring(
clientUpgrade.UpgradeFingerprint.indexOf("=") + 1,
clientUpgrade.UpgradeFingerprint.lastIndexOf("|")
)}}`;
}
// Handle Fusion Core drops
const parsedFingerprint = JSON.parse(clientUpgrade.UpgradeFingerprint) as { lvl: number };
if (parsedFingerprint.lvl != 0) {
inventory.Upgrades.push({
ItemType: clientUpgrade.ItemType,
UpgradeFingerprint: clientUpgrade.UpgradeFingerprint
});
} else if (id == "") {
// U19 does not provide RawUpgrades and instead interleaves them with riven progress here
addMods(inventory, [clientUpgrade]);
addMods(inventory, [
{
ItemType: clientUpgrade.ItemType,
ItemCount: clientUpgrade.ItemCount
}
]);
} else {
const upgrade = inventory.Upgrades.id(id)!;
upgrade.UpgradeFingerprint = clientUpgrade.UpgradeFingerprint; // primitive way to copy over the riven challenge progress

View File

@@ -554,6 +554,9 @@ const handleBoosterPackPurchase = async (
BoosterPackItems: "",
InventoryChanges: {}
};
if (quantity < 1) {
throw new Error(`invalid quantity for booster pack purchase: ${quantity}`);
}
if (quantity > 100) {
throw new Error(
"attempt to roll over 100 booster packs in a single go. possible but unlikely to be desirable for the user or the server."

View File

@@ -15,6 +15,7 @@ import type { IQuestKeyClient, IQuestKeyDatabase, IQuestStage } from "../types/i
import { logger } from "../utils/logger.ts";
import { ExportKeys, ExportRecipes } from "warframe-public-export-plus";
import { addFixedLevelRewards } from "./missionInventoryUpdateService.ts";
import { fromOid } from "../helpers/inventoryHelpers.ts";
import type { IInventoryChanges } from "../types/purchaseTypes.ts";
import questCompletionItems from "../../static/fixed_responses/questCompletionRewards.json" with { type: "json" };
import type { ITypeCount } from "../types/commonTypes.ts";
@@ -609,21 +610,18 @@ export const removeRequiredItems = async (inventory: TInventoryDatabaseDocument,
}
case "/Lotus/Types/Recipes/WarframeRecipes/ChromaBlueprint": {
await addItems(inventory, [
{
ItemType: "/Lotus/Types/Recipes/WarframeRecipes/ChromaBeaconABlueprint",
ItemCount: -1
},
{
ItemType: "/Lotus/Types/Recipes/WarframeRecipes/ChromaBeaconBBlueprint",
ItemCount: -1
},
{
ItemType: "/Lotus/Types/Recipes/WarframeRecipes/ChromaBeaconCBlueprint",
ItemCount: -1
const itemsToRemove = [
"/Lotus/Types/Recipes/WarframeRecipes/ChromaBeaconABlueprint",
"/Lotus/Types/Recipes/WarframeRecipes/ChromaBeaconBBlueprint",
"/Lotus/Types/Recipes/WarframeRecipes/ChromaBeaconCBlueprint"
];
for (const itemToRemove of itemsToRemove) {
try {
await addItem(inventory, itemToRemove, -1, undefined, undefined, undefined, true);
} catch (e) {
logger.debug(`removeRequiredItems: Couldn't remove ${itemToRemove}: ${(e as Error).message}`);
}
]);
}
break;
}
@@ -636,20 +634,20 @@ export const removeRequiredItems = async (inventory: TInventoryDatabaseDocument,
if (!inventory.MiscItems.find(i => i.ItemType == recipe.resultType)) {
await addItem(inventory, recipe.resultType);
if (recipe.consumeOnUse) await addItem(inventory, recipeItem.ItemType, -1);
await addItems(inventory, [
{
ItemType: "/Lotus/Types/Keys/BardQuest/BardQuestSequencerPartA",
ItemCount: -1
},
{
ItemType: "/Lotus/Types/Keys/BardQuest/BardQuestSequencerPartB",
ItemCount: -1
},
{
ItemType: "/Lotus/Types/Keys/BardQuest/BardQuestSequencerPartC",
ItemCount: -1
const itemsToRemove = [
"/Lotus/Types/Keys/BardQuest/BardQuestSequencerPartA",
"/Lotus/Types/Keys/BardQuest/BardQuestSequencerPartB",
"/Lotus/Types/Keys/BardQuest/BardQuestSequencerPartC"
];
for (const itemToRemove of itemsToRemove) {
try {
await addItem(inventory, itemToRemove, -1, undefined, undefined, undefined, true);
} catch (e) {
logger.debug(
`removeRequiredItems: Couldn't remove ${itemToRemove}: ${(e as Error).message}`
);
}
]);
}
}
}
break;
@@ -761,9 +759,9 @@ export const removeRequiredItems = async (inventory: TInventoryDatabaseDocument,
"",
"",
"",
umbraModA.ItemId.$oid,
umbraModB.ItemId.$oid,
umbraModC.ItemId.$oid
fromOid(umbraModA.ItemId),
fromOid(umbraModB.ItemId),
fromOid(umbraModC.ItemId)
]
}
],
@@ -778,7 +776,16 @@ export const removeRequiredItems = async (inventory: TInventoryDatabaseDocument,
addEquipment(inventory, "Melee", "/Lotus/Weapons/Tenno/Melee/Swords/UmbraKatana/UmbraKatana", {
Configs: [
{
Upgrades: ["", "", "", "", "", "", sacrificeModA.ItemId.$oid, sacrificeModB.ItemId.$oid]
Upgrades: [
"",
"",
"",
"",
"",
"",
fromOid(sacrificeModA.ItemId),
fromOid(sacrificeModB.ItemId)
]
}
],
XP: 450_000,

View File

@@ -6,14 +6,15 @@ import type {
ISaveLoadoutRequestNoUpgradeVer
} from "../types/saveLoadoutTypes.ts";
import { Loadout } from "../models/inventoryModels/loadoutModel.ts";
import { getInventory } from "./inventoryService.ts";
import { addMods, getInventory } from "./inventoryService.ts";
import type { IOid } from "../types/commonTypes.ts";
import { Types } from "mongoose";
import { isEmptyObject } from "../helpers/general.ts";
import { version_compare } from "../helpers/inventoryHelpers.ts";
import { logger } from "../utils/logger.ts";
import type { TEquipmentKey } from "../types/inventoryTypes/inventoryTypes.ts";
import { equipmentKeys } from "../types/inventoryTypes/inventoryTypes.ts";
import type { IItemConfig } from "../types/inventoryTypes/commonInventoryTypes.ts";
import type { IItemConfig, IItemConfigDatabase } from "../types/inventoryTypes/commonInventoryTypes.ts";
import { importCrewShipMembers, importCrewShipWeapon, importLoadOutConfig } from "./importService.ts";
//TODO: setup default items on account creation or like originally in giveStartingItems.php
@@ -26,7 +27,8 @@ itemconfig has multiple config ids
*/
export const handleInventoryItemConfigChange = async (
equipmentChanges: ISaveLoadoutRequestNoUpgradeVer,
accountId: string
accountId: string,
buildLabel: string | undefined
): Promise<string | void> => {
const inventory = await getInventory(accountId);
@@ -196,7 +198,36 @@ export const handleInventoryItemConfigChange = async (
for (const [configId, config] of Object.entries(itemConfigEntries)) {
if (/^[0-9]+$/.test(configId)) {
inventoryItem.Configs[parseInt(configId)] = config as IItemConfig;
const c = config as IItemConfig;
if (buildLabel && version_compare(buildLabel, "2014.04.10.17.47") < 0) {
if (c.Upgrades) {
// U10-U11 store mods in the item config as $id instead of a string, need to convert that here
const convertedUpgrades: string[] = [];
c.Upgrades.forEach(upgrade => {
const upgradeId = upgrade as { $id: string };
const rawUpgrade = inventory.RawUpgrades.id(upgradeId.$id);
if (rawUpgrade) {
const newId = new Types.ObjectId();
convertedUpgrades.push(newId.toString());
addMods(inventory, [
{
ItemType: rawUpgrade.ItemType,
ItemCount: -1
}
]);
inventory.Upgrades.push({
UpgradeFingerprint: `{"lvl":0}`,
ItemType: rawUpgrade.ItemType,
_id: newId
});
} else {
convertedUpgrades.push(upgradeId.$id);
}
});
c.Upgrades = convertedUpgrades;
}
}
inventoryItem.Configs[parseInt(configId)] = c as IItemConfigDatabase;
}
}
if ("Favorite" in itemConfigEntries) {

View File

@@ -115,6 +115,8 @@ const sortieBossNode: Record<Exclude<TSortieBoss, "SORTIE_BOSS_CORRUPTED_VOR">,
SORTIE_BOSS_VOR: "SolNode108"
};
const sortieAssassinationOnlyNodes: string[] = ["SolNode193", "SolNode105", "SolNode108"];
const configAlerts: Record<string, IAlert> = {
voidCorruption2025Week1: {
_id: { $oid: "677d452e2f324ee7b90f8ccf" },
@@ -381,8 +383,11 @@ export const getSortie = (day: number): ISortie => {
}
for (let i = 0; i < 3; i++) {
const randomIndex = rng.randomInt(0, nodes.length - 1);
const node = nodes[randomIndex];
let randomIndex, node;
do {
randomIndex = rng.randomInt(0, nodes.length - 1);
node = nodes[randomIndex];
} while (sortieAssassinationOnlyNodes.indexOf(node) != -1);
const modifiers = [
"SORTIE_MODIFIER_LOW_ENERGY",

View File

@@ -4,6 +4,7 @@ import type {
ICrewShipCustomization,
IFlavourItem,
IItemConfig,
IItemConfigDatabase,
IPolarity
} from "./inventoryTypes/commonInventoryTypes.ts";
@@ -33,7 +34,7 @@ export enum EquipmentFeatures {
export interface IEquipmentDatabase {
ItemType: string;
ItemName?: string;
Configs: IItemConfig[];
Configs: IItemConfigDatabase[];
UpgradeVer?: number;
XP?: number;
Features?: number;
@@ -48,7 +49,6 @@ export interface IEquipmentDatabase {
InfestationDays?: number;
InfestationType?: string;
ModularParts?: string[];
UnlockLevel?: number;
Expiry?: Date;
SkillTree?: string;
OffensiveUpgrade?: string;
@@ -69,9 +69,18 @@ export interface IEquipmentDatabase {
export interface IEquipmentClient
extends Omit<
IEquipmentDatabase,
"_id" | "InfestationDate" | "Expiry" | "UpgradesExpiry" | "UmbraDate" | "Weapon" | "CrewMembers" | "Details"
| "_id"
| "Configs"
| "InfestationDate"
| "Expiry"
| "UpgradesExpiry"
| "UmbraDate"
| "Weapon"
| "CrewMembers"
| "Details"
> {
ItemId: IOidWithLegacySupport;
Configs: IItemConfig[];
InfestationDate?: IMongoDate;
Expiry?: IMongoDate;
UpgradesExpiry?: IMongoDate;
@@ -79,6 +88,10 @@ export interface IEquipmentClient
Weapon?: ICrewShipWeaponClient;
CrewMembers?: ICrewShipMembersClient;
Details?: IKubrowPetDetailsClient;
// For Pre-U24.4.0 builds
UnlockLevel?: number;
UtilityUnlocked?: number;
Gild?: boolean;
}
export interface IArchonCrystalUpgrade {

View File

@@ -47,7 +47,7 @@ export interface IItemConfig {
facial?: IColor;
syancol?: IColor;
cloth?: IColor;
Upgrades?: string[];
Upgrades?: string[] | { $id: string }[];
Name?: string;
OperatorAmp?: IOid;
Songs?: ISong[];
@@ -56,13 +56,17 @@ export interface IItemConfig {
ugly?: boolean;
}
export interface IItemConfigDatabase extends Omit<IItemConfig, "Upgrades"> {
Upgrades?: string[];
}
export interface ISong {
m?: string;
b?: string;
p?: string;
s: string;
}
export interface IOperatorConfigDatabase extends IItemConfig {
export interface IOperatorConfigDatabase extends IItemConfigDatabase {
_id: Types.ObjectId;
}

View File

@@ -23,6 +23,7 @@ export type InventoryDatabaseEquipment = {
// Fields specific to SNS
export interface IAccountCheats {
skipAllDialogue?: boolean;
skipAllPopups?: boolean;
dontSubtractPurchaseCreditCost?: boolean;
dontSubtractPurchasePlatinumCost?: boolean;
dontSubtractPurchaseItemCost?: boolean;
@@ -317,6 +318,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
Accolades?: IAccolades;
Counselor?: boolean;
Upgrades: IUpgradeClient[];
Cards?: IUpgradeClient[]; // U8
EquippedGear: string[];
DeathMarks: string[];
FusionTreasures: IFusionTreasure[];
@@ -591,10 +593,16 @@ export interface IUpgradeClient {
ItemType: string;
UpgradeFingerprint?: string;
PendingRerollFingerprint?: string;
ItemId: IOid;
ItemId: IOidWithLegacySupport;
// Stuff for U7-U8
ParentId?: IOidWithLegacySupport;
Slot?: number;
AmountRemaining?: number;
Rank?: number;
}
export interface IUpgradeDatabase extends Omit<IUpgradeClient, "ItemId"> {
export interface IUpgradeDatabase
extends Omit<IUpgradeClient, "ItemId" | "ParentId" | "Slot" | "AmountRemaining" | "Rank"> {
_id: Types.ObjectId;
}
@@ -602,9 +610,9 @@ export interface IUpgradeFromClient {
ItemType: string;
ItemId: IOidWithLegacySupport;
FromSKU?: boolean;
UpgradeFingerprint: string;
UpgradeFingerprint?: string;
PendingRerollFingerprint: string;
ItemCount: number;
ItemCount?: number;
LastAdded: IOidWithLegacySupport;
}

View File

@@ -1,4 +1,4 @@
import type { IOid, ITypeCount } from "./commonTypes.ts";
import type { IOid, IOidWithLegacySupport, ITypeCount } from "./commonTypes.ts";
import type { ArtifactPolarity, IPolarity } from "./inventoryTypes/commonInventoryTypes.ts";
import type {
IBooster,
@@ -21,7 +21,8 @@ import type {
IInvasionProgressClient,
IWeaponSkinClient,
IKubrowPetEggClient,
INemesisClient
INemesisClient,
IUpgradeClient
} from "./inventoryTypes/inventoryTypes.ts";
import type { IGroup } from "./loginTypes.ts";
import type { ILoadOutPresets } from "./saveLoadoutTypes.ts";
@@ -227,6 +228,24 @@ export interface IUpgradesRequest {
UpgradeVersion: number;
Operations: IUpgradeOperation[];
}
export interface IUpgradesRequestLegacy {
Category: TEquipmentKey;
Weapon: { ItemType: string; ItemId: IOidWithLegacySupport };
UpgradeVer: number;
UnlockLevel: number;
Polarized: number;
UtilityUnlocked: number;
FocusLens?: string;
UpgradeReq?: string;
UtilityReq?: string;
IsSwappingOperation: boolean;
PolarizeReq?: string;
PolarizeSlot: number;
PolarizeValue: ArtifactPolarity;
PolarityRemap: IPolarity[];
UpgradesToAttach?: IUpgradeClient[];
UpgradesToDetach?: IUpgradeClient[];
}
export interface IUpgradeOperation {
OperationType: string;
UpgradeRequirement: string; // uniqueName of item being consumed

View File

@@ -31,7 +31,7 @@ export interface ISession {
export interface IFindSessionRequest {
id?: string;
originalSessionId?: string;
buildId?: number;
buildId?: number | bigint;
gameModeId?: number;
regionId?: number;
maxEloDifference?: number;

View File

@@ -1,4 +1,6 @@
[
"ArcaneWallConsole",
"CetusHub4",
"SolarisUnitedHub1",
"/Lotus/Language/SolarisVenus/FishmongerName",
"/Lotus/Language/SolarisVenus/ProspectorName",
@@ -12,6 +14,7 @@
"SaturnWolf3",
"SaturnWolf4",
"SaturnWolf5",
"SyndicateFirstPledge",
"ConclaveSyndicate",
"ArbitersSyndicate",
"LibrarySyndicate",
@@ -117,6 +120,7 @@
"/Lotus/Language/EntratiLab/EntratiGeneral/Fibonacci",
"/Lotus/Language/EntratiLab/EntratiGeneral/BirdThree",
"/Lotus/Language/Zariman/Quinn",
"/Lotus/Language/EntratiLab/EntratiGeneral/Tagfer",
"/Lotus/Language/EntratiLab/EntratiGeneral/TagferFirstRank1",
"VoidVaultIntro",
"PurchasePlatformLockedNotificationSeen",
@@ -137,6 +141,7 @@
"EntratiLabConquestHardModeUnlocked",
"/Lotus/Language/Npcs/KonzuPostNewWar",
"/Lotus/Language/SolarisVenus/EudicoPostNewWar",
"/Lotus/Language/FiveFates/KoumeiStatueHubName",
"/Lotus/Language/NokkoColony/NokkoVendorName",
"NokkoVisions_FirstVisit"
]

View File

@@ -0,0 +1,27 @@
[
"RailjackPlexusTutorial",
"RailjackIntrinsicsTutorial",
"RailjackDryDockTutorial",
"RailjackStarchartTutorial",
"NPE_poponce_ayatans",
"NPE_poponce_sellmodinfo",
"NPE_poponce_transmuteinfo",
"MarketOpened",
"PrimeTokensTutorial",
"WelcomeScreen_Undermind",
"WelcomeScreen_Jade",
"WelcomeScreen_Isleweaver",
"WelcomeScreen_Techrot",
"WelcomeScreen_1999",
"WelcomeScreen_Koumei",
"WelcomeScreen_Dante",
"WelcomeScreen_Whispers",
"WelcomeScreen_Dagath",
"WelcomeScreen_Kullervo",
"EpisodeIntro_RadioLegionIntermission14Syndicate",
"EpisodeIntro_RadioLegionIntermission13Syndicate",
"EpisodeIntro_RadioLegionIntermission12Syndicate",
"EpisodeIntro_RadioLegionIntermission11Syndicate",
"EpisodeIntro_RadioLegionIntermission10Syndicate",
"EpisodeIntro_RadioLegionIntermission9Syndicate"
]

View File

@@ -960,6 +960,10 @@
<input class="form-check-input" type="checkbox" id="skipAllDialogue" />
<label class="form-check-label" for="skipAllDialogue" data-loc="cheats_skipAllDialogue"></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="skipAllPopups" />
<label class="form-check-label" for="skipAllPopups" data-loc="cheats_skipAllPopups"></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="dontSubtractPurchaseCreditCost" />
<label class="form-check-label" for="dontSubtractPurchaseCreditCost" data-loc="cheats_dontSubtractPurchaseCreditCost"></label>
@@ -1558,6 +1562,7 @@
<p class="mt-3 mb-1" data-loc="import_samples"></p>
<ul>
<li><a href="#" onclick="event.preventDefault();setImportSample('maxFocus');" data-loc="import_samples_maxFocus"></a></li>
<li><a href="#" onclick="event.preventDefault();setImportSample('accolades');" data-loc="import_samples_accolades"></a></li>
</ul>
</div>
</div>

View File

@@ -426,9 +426,25 @@ function fetchItemList() {
};
// Add mods missing in data sources
data.mods.push({
uniqueName: "/Lotus/Upgrades/Mods/Fusers/CommonModFuser",
name: loc("code_fusionCoreCommon"),
fusionLimit: 3
});
data.mods.push({
uniqueName: "/Lotus/Upgrades/Mods/Fusers/UncommonModFuser",
name: loc("code_fusionCoreUncommon"),
fusionLimit: 5
});
data.mods.push({
uniqueName: "/Lotus/Upgrades/Mods/Fusers/RareModFuser",
name: loc("code_fusionCoreRare"),
fusionLimit: 5
});
data.mods.push({
uniqueName: "/Lotus/Upgrades/Mods/Fusers/LegendaryModFuser",
name: loc("code_legendaryCore")
name: loc("code_legendaryCore"),
fusionLimit: 0
});
data.mods.push({
uniqueName: "/Lotus/Upgrades/CosmeticEnhancers/Peculiars/CyoteMod",
@@ -472,6 +488,12 @@ function fetchItemList() {
name: loc("disabled")
});
data.Boosters ??= [];
data.Boosters.push({
uniqueName: "/Lotus/Types/Boosters/ReviveBooster",
name: loc("code_reviveBooster")
});
const itemMap = {
// Generics for rivens
"/Lotus/Weapons/Tenno/Archwing/Primary/ArchGun": { name: loc("code_archgun") },
@@ -1053,6 +1075,13 @@ function updateInventory() {
const td = document.createElement("td");
td.textContent = itemMap[item.ItemType]?.name ?? item.ItemType;
tr.appendChild(td);
if (item.ItemType == "/Lotus/Types/Boosters/ReviveBooster") {
td.textContent += " ";
const abbr = document.createElement("abbr");
abbr.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M320 576C461.4 576 576 461.4 576 320C576 178.6 461.4 64 320 64C178.6 64 64 178.6 64 320C64 461.4 178.6 576 320 576zM320 200C333.3 200 344 210.7 344 224L344 336C344 349.3 333.3 360 320 360C306.7 360 296 349.3 296 336L296 224C296 210.7 306.7 200 320 200zM293.3 416C292.7 406.1 297.6 396.7 306.1 391.5C314.6 386.4 325.3 386.4 333.8 391.5C342.3 396.7 347.2 406.1 346.6 416C347.2 425.9 342.3 435.3 333.8 440.5C325.3 445.6 314.6 445.6 306.1 440.5C297.6 435.3 292.7 425.9 293.3 416z"/></svg>`;
abbr.title = loc("code_reviveBoosterDesc");
td.appendChild(abbr);
}
}
{
const td = document.createElement("td");
@@ -1494,7 +1523,9 @@ function updateInventory() {
{
const td = document.createElement("td");
td.textContent = itemMap[item.ItemType]?.name ?? item.ItemType;
td.innerHTML += " <span title='" + loc("code_rank") + "'>★ 0/" + maxRank + "</span>";
if (maxRank > 0) {
td.innerHTML += " <span title='" + loc("code_rank") + "'>★ 0/" + maxRank + "</span>";
}
if (item.ItemCount > 1) {
td.innerHTML +=
" <span title='" + loc("code_count") + "'>🗍 " + parseInt(item.ItemCount) + "</span>";
@@ -3450,6 +3481,9 @@ function doAddAllMods() {
for (const child of document.getElementById("datalist-mods").children) {
modsAll.add(child.getAttribute("data-key"));
}
modsAll.delete("/Lotus/Upgrades/Mods/Fusers/CommonModFuser");
modsAll.delete("/Lotus/Upgrades/Mods/Fusers/UncommonModFuser");
modsAll.delete("/Lotus/Upgrades/Mods/Fusers/RareModFuser");
modsAll.delete("/Lotus/Upgrades/Mods/Fusers/LegendaryModFuser");
revalidateAuthz().then(() => {
@@ -3612,8 +3646,12 @@ function doImport() {
data: JSON.stringify({
inventory: JSON.parse($("#import-inventory").val())
})
}).then(function (anyKnownKey) {
toast(loc(anyKnownKey ? "code_succImport" : "code_nothingToDo"));
}).then(function (err) {
if (err) {
toast(err == "noKnownKey" ? loc("code_nothingToDo") : err);
} else {
toast(loc("code_succImport"));
}
updateInventory();
});
} catch (e) {
@@ -4113,6 +4151,22 @@ const importSamples = {
Level: 3
}
]
},
accolades: {
Staff: false,
Founder: 4,
Guide: 2,
Moderator: true,
Partner: true,
Created: {
$date: {
$numberLong: "1356998400000"
}
},
Accolades: {
Heirloom: true
},
Counselor: true
}
};
function setImportSample(key) {

View File

@@ -23,6 +23,9 @@ dict = {
code_moteAmp: `Anfangsverstärker`,
code_amp: `Verstärker`,
code_kDrive: `K-Drive`,
code_fusionCoreCommon: `[UNTRANSLATED] Fusion Core (Common)`,
code_fusionCoreUncommon: `[UNTRANSLATED] Fusion Core (Uncommon)`,
code_fusionCoreRare: `[UNTRANSLATED] Fusion Core (Rare)`,
code_legendaryCore: `Legendärer Kern`,
code_traumaticPeculiar: `Kuriose Mod: Traumatisch`,
code_starter: `|MOD| (Defekt)`,
@@ -79,6 +82,8 @@ dict = {
code_drifterBeardName: `Drifter-Bart: |INDEX|`,
code_drifterFaceName: `Drifter-Gesicht: |INDEX|`,
code_operatorFaceName: `Operator-Gesicht: |INDEX|`,
code_reviveBooster: `Wiederbelebungsbooster`,
code_reviveBoosterDesc: `[UNTRANSLATED] Sets revive count to 4, which allows self-revive in Archon Hunts.`,
code_succChange: `Erfolgreich geändert.`,
code_requiredInvigorationUpgrade: `Du musst sowohl ein Offensiv- als auch ein Support-Upgrade auswählen.`,
code_feature_1: `[UNTRANSLATED] Orokin Reactor`,
@@ -210,6 +215,7 @@ dict = {
cheats_server: `Server`,
cheats_skipTutorial: `Tutorial überspringen`,
cheats_skipAllDialogue: `Alle Dialoge überspringen`,
cheats_skipAllPopups: `Alle Popups überspringen`,
cheats_unlockAllScans: `Alle Scans freischalten`,
cheats_unlockSuccRelog: `Erfolgreich. Bitte beachte, dass du dich neu anmelden musst, damit der Client dies aktualisiert.`,
cheats_unlockAllMissions: `Alle Missionen freischalten`,
@@ -368,6 +374,7 @@ dict = {
import_submit: `Absenden`,
import_samples: `Beispiele:`,
import_samples_maxFocus: `Alle Fokus-Schulen maximiert`,
import_samples_accolades: `Auszeichnungen & Council Chat Zugang`,
upgrade_Equilibrium: `+|VAL|% Energie bei Gesundheitskugeln, +|VAL|% Gesundheit bei Energiekugeln`,
upgrade_MeleeCritDamage: `+|VAL|% Krit. Nahkampfschaden`,

View File

@@ -22,6 +22,9 @@ dict = {
code_moteAmp: `Mote Amp`,
code_amp: `Amp`,
code_kDrive: `K-Drive`,
code_fusionCoreCommon: `Fusion Core (Common)`,
code_fusionCoreUncommon: `Fusion Core (Uncommon)`,
code_fusionCoreRare: `Fusion Core (Rare)`,
code_legendaryCore: `Legendary Core`,
code_traumaticPeculiar: `Traumatic Peculiar`,
code_starter: `|MOD| (Flawed)`,
@@ -78,6 +81,8 @@ dict = {
code_drifterBeardName: `Drifter Beard |INDEX|`,
code_drifterFaceName: `Drifter Visage |INDEX|`,
code_operatorFaceName: `Operator Visage |INDEX|`,
code_reviveBooster: `Revive Booster`,
code_reviveBoosterDesc: `Sets revive count to 4, which allows self-revive in Archon Hunts.`,
code_succChange: `Successfully changed.`,
code_requiredInvigorationUpgrade: `You must select both an offensive & utility upgrade.`,
code_feature_1: `Orokin Reactor`,
@@ -209,6 +214,7 @@ dict = {
cheats_server: `Server`,
cheats_skipTutorial: `Skip Tutorial`,
cheats_skipAllDialogue: `Skip All Dialogue`,
cheats_skipAllPopups: `Skip All Popups`,
cheats_unlockAllScans: `Unlock All Scans`,
cheats_unlockSuccRelog: `Success. Please note that you'll need to relog for the client to refresh this.`,
cheats_unlockAllMissions: `Unlock All Missions`,
@@ -367,6 +373,7 @@ dict = {
import_submit: `Submit`,
import_samples: `Samples:`,
import_samples_maxFocus: `All Focus Schools Maxed Out`,
import_samples_accolades: `Accolades & Council Chat Access`,
upgrade_Equilibrium: `+|VAL|% Energy from Health pickups, +|VAL|% Health from Energy pickups`,
upgrade_MeleeCritDamage: `+|VAL|% Melee Critical Damage`,

View File

@@ -23,6 +23,9 @@ dict = {
code_moteAmp: `Amp Mota`,
code_amp: `Amp`,
code_kDrive: `K-Drive`,
code_fusionCoreCommon: `[UNTRANSLATED] Fusion Core (Common)`,
code_fusionCoreUncommon: `[UNTRANSLATED] Fusion Core (Uncommon)`,
code_fusionCoreRare: `[UNTRANSLATED] Fusion Core (Rare)`,
code_legendaryCore: `Núcleo legendario`,
code_traumaticPeculiar: `Traumatismo peculiar`,
code_starter: `|MOD| (Defectuoso)`,
@@ -79,6 +82,8 @@ dict = {
code_drifterBeardName: `Barba del Viajero: |INDEX|`,
code_drifterFaceName: `Rostro del Viajero |INDEX|`,
code_operatorFaceName: `Rostro del operador |INDEX|`,
code_reviveBooster: `Potenciador de reanimaciones`,
code_reviveBoosterDesc: `[UNTRANSLATED] Sets revive count to 4, which allows self-revive in Archon Hunts.`,
code_succChange: `Cambiado correctamente`,
code_requiredInvigorationUpgrade: `Debes seleccionar una mejora ofensiva y una mejora de utilidad.`,
code_feature_1: `[UNTRANSLATED] Orokin Reactor`,
@@ -210,6 +215,7 @@ dict = {
cheats_server: `Servidor`,
cheats_skipTutorial: `Omitir tutorial`,
cheats_skipAllDialogue: `Omitir todos los diálogos`,
cheats_skipAllPopups: `[UNTRANSLATED] Skip All Popups`,
cheats_unlockAllScans: `Desbloquear todos los escaneos`,
cheats_unlockSuccRelog: `Éxito. Ten en cuenta que deberás volver a iniciar sesión para que el cliente se actualice.`,
cheats_unlockAllMissions: `Desbloquear todas las misiones`,
@@ -368,6 +374,7 @@ dict = {
import_submit: `Enviar`,
import_samples: `Muestras:`,
import_samples_maxFocus: `Todas las escuelas de enfoque al máximo`,
import_samples_accolades: `[UNTRANSLATED] Accolades & Council Chat Access`,
upgrade_Equilibrium: `+|VAL|% de Energía al recoger salud, +|VAL|% de Salud al recoger energía`,
upgrade_MeleeCritDamage: `+|VAL|% de daño crítico cuerpo a cuerpo`,

View File

@@ -23,6 +23,9 @@ dict = {
code_moteAmp: `Amplificateur Faible`,
code_amp: `Amplificateur`,
code_kDrive: `K-Drive`,
code_fusionCoreCommon: `[UNTRANSLATED] Fusion Core (Common)`,
code_fusionCoreUncommon: `[UNTRANSLATED] Fusion Core (Uncommon)`,
code_fusionCoreRare: `[UNTRANSLATED] Fusion Core (Rare)`,
code_legendaryCore: `Coeur Légendaire`,
code_traumaticPeculiar: `Traumatisme Atypique`,
code_starter: `|MOD| (Défectueux)`,
@@ -79,6 +82,8 @@ dict = {
code_drifterBeardName: `Barbe du Voyageur |INDEX|`,
code_drifterFaceName: `Visage du Voyageur |INDEX|`,
code_operatorFaceName: `Visage de l'Opérateur |INDEX|`,
code_reviveBooster: `Booster de Réanimation`,
code_reviveBoosterDesc: `[UNTRANSLATED] Sets revive count to 4, which allows self-revive in Archon Hunts.`,
code_succChange: `Changement effectué.`,
code_requiredInvigorationUpgrade: `Invigoration offensive et défensive requises.`,
code_feature_1: `[UNTRANSLATED] Orokin Reactor`,
@@ -210,6 +215,7 @@ dict = {
cheats_server: `Serveur`,
cheats_skipTutorial: `Passer le tutoriel`,
cheats_skipAllDialogue: `Passer les dialogues`,
cheats_skipAllPopups: `[UNTRANSLATED] Skip All Popups`,
cheats_unlockAllScans: `Débloquer tous les scans`,
cheats_unlockSuccRelog: `Succès. Une reconnexion est requise pour appliquer les changements.`,
cheats_unlockAllMissions: `Débloquer toutes les missions`,
@@ -368,6 +374,7 @@ dict = {
import_submit: `Soumettre`,
import_samples: `Échantillons :`,
import_samples_maxFocus: `Toutes les écoles de focus au rang max`,
import_samples_accolades: `[UNTRANSLATED] Accolades & Council Chat Access`,
upgrade_Equilibrium: `Ramasser de la santé donne +|VAL|% d'énergie supplémentaire. Ramasser de l'énergie donne +|VAL|% de santé supplémentaire.`,
upgrade_MeleeCritDamage: `+|VAL|% de dégâts critique en mêlée`,

View File

@@ -23,6 +23,9 @@ dict = {
code_moteAmp: `Пылинка`,
code_amp: `Усилитель`,
code_kDrive: `К-Драйв`,
code_fusionCoreCommon: `[UNTRANSLATED] Fusion Core (Common)`,
code_fusionCoreUncommon: `[UNTRANSLATED] Fusion Core (Uncommon)`,
code_fusionCoreRare: `[UNTRANSLATED] Fusion Core (Rare)`,
code_legendaryCore: `Легендарное ядро`,
code_traumaticPeculiar: `Травмирующая Странность`,
code_starter: `|MOD| (Повреждённый)`,
@@ -79,6 +82,8 @@ dict = {
code_drifterBeardName: `Борода скитальца: |INDEX|`,
code_drifterFaceName: `Внешность скитальца: |INDEX|`,
code_operatorFaceName: `Внешность оператора: |INDEX|`,
code_reviveBooster: `Усилитель возрождения`,
code_reviveBoosterDesc: `Устанавливает количество возрождений на 4. Снимает ограничение на самовоскрешение на миссиях Охоты на архонтов.`,
code_succChange: `Успешно изменено.`,
code_requiredInvigorationUpgrade: `Вы должны выбрать как атакующее, так и вспомогательное улучшение.`,
code_feature_1: `Реактор Орокин`,
@@ -210,6 +215,7 @@ dict = {
cheats_server: `Сервер`,
cheats_skipTutorial: `Пропустить обучение`,
cheats_skipAllDialogue: `Пропустить все диалоги`,
cheats_skipAllPopups: `[UNTRANSLATED] Skip All Popups`,
cheats_unlockAllScans: `Разблокировать все сканирования`,
cheats_unlockSuccRelog: `Успех. Вам необходимо повторно войти в игру, чтобы клиент обновил эту информацию.`,
cheats_unlockAllMissions: `Разблокировать все миссии`,
@@ -368,6 +374,7 @@ dict = {
import_submit: `Отправить`,
import_samples: `Пример:`,
import_samples_maxFocus: `Все школы Фокуса макс. уровня`,
import_samples_accolades: `[UNTRANSLATED] Accolades & Council Chat Access`,
upgrade_Equilibrium: `Подбор сфер здоровья даёт +|VAL|% энергии. Подбор сфер энергии даёт +|VAL|% здоровья.`,
upgrade_MeleeCritDamage: `+|VAL|% к крит. урону в ближнем бою.`,

View File

@@ -23,6 +23,9 @@ dict = {
code_moteAmp: `Порошинка`,
code_amp: `Підсилювач`,
code_kDrive: `К-Драйв`,
code_fusionCoreCommon: `[UNTRANSLATED] Fusion Core (Common)`,
code_fusionCoreUncommon: `[UNTRANSLATED] Fusion Core (Uncommon)`,
code_fusionCoreRare: `[UNTRANSLATED] Fusion Core (Rare)`,
code_legendaryCore: `Легендарне ядро`,
code_traumaticPeculiar: `Особливе травмування`,
code_starter: `|MOD| (Пошкоджений)`,
@@ -79,6 +82,8 @@ dict = {
code_drifterBeardName: `Борода мандрівника: |INDEX|`,
code_drifterFaceName: `Зовнішність мандрівника: |INDEX|`,
code_operatorFaceName: `Зовнішність оператора: |INDEX|`,
code_reviveBooster: `Збільшувач зцілення`,
code_reviveBoosterDesc: `[UNTRANSLATED] Sets revive count to 4, which allows self-revive in Archon Hunts.`,
code_succChange: `Успішно змінено.`,
code_requiredInvigorationUpgrade: `Ви повинні вибрати як атакуюче, так і допоміжне вдосконалення.`,
code_feature_1: `[UNTRANSLATED] Orokin Reactor`,
@@ -210,6 +215,7 @@ dict = {
cheats_server: `Сервер`,
cheats_skipTutorial: `Пропустити навчання`,
cheats_skipAllDialogue: `Пропустити всі діалоги`,
cheats_skipAllPopups: `[UNTRANSLATED] Skip All Popups`,
cheats_unlockAllScans: `Розблокувати всі сканування`,
cheats_unlockSuccRelog: `Успіх. Вам потрібно буде повторно увійти в гру, щоб клієнт оновив цю інформацію.`,
cheats_unlockAllMissions: `Розблокувати всі місії`,
@@ -368,6 +374,7 @@ dict = {
import_submit: `Відправити`,
import_samples: `Приклад:`,
import_samples_maxFocus: `Всі школи Фокусу макс. рівня`,
import_samples_accolades: `[UNTRANSLATED] Accolades & Council Chat Access`,
upgrade_Equilibrium: `Згустки здоров'я дають +|VAL|% енергії, згустки енергії дають +|VAL|% здоров'я.`,
upgrade_MeleeCritDamage: `+|VAL|% до критичної шкоди від холодної зброї.`,

View File

@@ -23,6 +23,9 @@ dict = {
code_moteAmp: `微尘增幅器`,
code_amp: `增幅器`,
code_kDrive: `K式悬浮板`,
code_fusionCoreCommon: `[UNTRANSLATED] Fusion Core (Common)`,
code_fusionCoreUncommon: `[UNTRANSLATED] Fusion Core (Uncommon)`,
code_fusionCoreRare: `[UNTRANSLATED] Fusion Core (Rare)`,
code_legendaryCore: `传奇核心`,
code_traumaticPeculiar: `创伤怪奇`,
code_starter: `|MOD| (有瑕疵的)`,
@@ -79,6 +82,8 @@ dict = {
code_drifterBeardName: `漂泊者胡须 |INDEX|`,
code_drifterFaceName: `漂泊者面部 |INDEX|`,
code_operatorFaceName: `指挥官面部 |INDEX|`,
code_reviveBooster: `复活加速器`,
code_reviveBoosterDesc: `[UNTRANSLATED] Sets revive count to 4, which allows self-revive in Archon Hunts.`,
code_succChange: `更改成功`,
code_requiredInvigorationUpgrade: `[UNTRANSLATED] You must select both an offensive & utility upgrade.`,
code_feature_1: `[UNTRANSLATED] Orokin Reactor`,
@@ -210,6 +215,7 @@ dict = {
cheats_server: `服务器`,
cheats_skipTutorial: `跳过教程`,
cheats_skipAllDialogue: `跳过所有对话`,
cheats_skipAllPopups: `[UNTRANSLATED] Skip All Popups`,
cheats_unlockAllScans: `解锁所有扫描`,
cheats_unlockSuccRelog: `[UNTRANSLATED] Success. Please note that you'll need to relog for the client to refresh this.`,
cheats_unlockAllMissions: `解锁所有星图`,
@@ -368,6 +374,7 @@ dict = {
import_submit: `提交`,
import_samples: `示例:`,
import_samples_maxFocus: `所有专精学派完全精通`,
import_samples_accolades: `[UNTRANSLATED] Accolades & Council Chat Access`,
upgrade_Equilibrium: `拾取生命球+|VAL|%能量,拾取能量球+|VAL|%生命`,
upgrade_MeleeCritDamage: `+|VAL|%近战暴击伤害`,

View File

@@ -1 +0,0 @@
nounusedlocals,