33 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
2eb7f7ed11 fix: use polling for chokidar when running under docker (#3033)
All checks were successful
Build / build (push) Successful in 1m9s
Build Docker image / docker (push) Successful in 2m48s
Closes #3031

Reviewed-on: #3033
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-11-15 00:26:32 -08:00
f3a6f60222 chore: remove networking config options when running under docker (#3032)
Some checks failed
Build / build (push) Has been cancelled
Build Docker image / docker (push) Has been cancelled
This should make it hopefully ever so slightly less confusing.

Reviewed-on: #3032
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-11-15 00:26:23 -08:00
55d051ff37 fix: item count validation when starting a recipe (#3030)
Some checks failed
Build Docker image / docker (push) Has been cancelled
Build / build (push) Has been cancelled
Can't assume it's all MiscItems. Instead upgraded the warnings to errors in the inventoryService functions.

Reviewed-on: #3030
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-11-15 00:26:14 -08:00
9f94a17eda chore: add tunables to config-vanilla.json (#3028)
Some checks failed
Build Docker image / docker (push) Has been cancelled
Build / build (push) Has been cancelled
Kinda forgor about this file when adding these options.

Reviewed-on: #3028
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-11-15 00:26:06 -08:00
711eb7ac47 fix: Shrine Defense node not showing up despite unlocking all missions via cheat (#3027)
All checks were successful
Build / build (push) Successful in 1m12s
Build Docker image / docker (push) Successful in 2m49s
Reviewed-on: #3027
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-14 01:50:33 -08:00
90ffd8948b fix: claiming recipes in U22.20-U24.1 + disable rush cost scaling for builds older than U18 (#3024)
Some checks failed
Build / build (push) Has started running
Build Docker image / docker (push) Has been cancelled
With this, the Foundry should be fully functional in all game versions now (excluding incorrect recipe data for things that got changed over the years).
This also disables rush cost scaling for versions older than U18, as U18 is the version that introduced it.

Reviewed-on: #3024
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-14 01:50:19 -08:00
8a0f99f573 fix: selling items in old builds (#3023)
Some checks failed
Build / build (push) Has been cancelled
Build Docker image / docker (push) Has been cancelled
I'm not sure exactly which build the sell request changed in, but this solution seems to be catch-all.

Reviewed-on: #3023
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-14 01:50:03 -08:00
55e2871531 fix: give ChallengeProgress with unlockAllScans cheat (#3020)
Some checks failed
Build / build (push) Has been cancelled
Build Docker image / docker (push) Has been cancelled
Closes #3013

Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Reviewed-on: #3020
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-14 01:49:48 -08:00
8088044ec8 feat(webui): echoes of umbra (#3019)
All checks were successful
Build / build (push) Successful in 2m2s
Build Docker image / docker (push) Successful in 1m45s
Reviewed-on: #3019
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-11 23:26:01 -08:00
6167eeadb0 feat: view leaderboard U8 (#3017)
Some checks failed
Build / build (push) Has been cancelled
Build Docker image / docker (push) Has been cancelled
Reviewed-on: #3017
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-11 23:25:40 -08:00
9c1ba17537 chore: automatically use nora's mix 6 & 7 in respective versions (#3016)
All checks were successful
Build / build (push) Successful in 1m18s
Build Docker image / docker (push) Successful in 2m45s
Reviewed-on: #3016
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-11-10 23:51:13 -08:00
85a446f67f fix: acquisition of new KubrowEgg item (#3015)
Some checks failed
Build / build (push) Has been cancelled
Build Docker image / docker (push) Has been cancelled
Reviewed-on: #3015
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-11-10 23:50:59 -08:00
48b7138c1c feat: initial foundry for U8 (#3014)
Some checks failed
Build Docker image / docker (push) Has been cancelled
Build / build (push) Has been cancelled
endpoints should work, but we don't have data for required recipe items for U8, so in most cases, an unknown error will occur in the game, and a more detailed error will occur in the server console.

Reviewed-on: #3014
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-10 23:50:51 -08:00
58 changed files with 1405 additions and 501 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>.

View File

@@ -14,6 +14,7 @@ SpaceNinjaServer requires a `config.json`. To set it up, you can copy the [confi
- `skipTutorial` affects only newly created accounts, so you may wish to change it before logging in for the first time.
- `logger.level` can be `fatal`, `error`, `warn`, `info`, `http`, `debug`, or `trace`.
- `bindAddress`, `httpPort`, `httpsPort` are related to how SpaceNinjaServer is reached on the network. Under Docker, these options are unchangable; modify your `docker-compose.yml`, instead.
- `ircExecutable` can be provided with a relative path to an EXE which will be ran as a child process of SpaceNinjaServer.
- `ircAddress`, `hubAddress`, and `nrsAddress` can be provided if these secondary servers are on a different machine.
- `worldState.eidolonOverride` can be set to `day` or `night` to lock the time to day/fass and night/vome on Plains of Eidolon/Cambion Drift.

View File

@@ -68,6 +68,14 @@
"circuitGameModes": null,
"darvoStockMultiplier": 1
},
"tunables": {
"useLoginToken": false,
"prohibitSkipMissionStartTimer": false,
"prohibitFovOverride": false,
"prohibitFreecam": false,
"prohibitTeleport": false,
"prohibitScripts": false
},
"dev": {
"keepVendorsExpired": false
}

View File

@@ -7,13 +7,16 @@ services:
- ./docker-data/static-data:/app/static/data
- ./docker-data/logs:/app/logs
ports:
# The lefthand value determines the port you actually connect to. Within the container, SpaceNinjaServer will always use 80 and 443 (righthand values).
- 80:80
- 443:443
# Normally, the image is fetched from Docker Hub, but you can use the local Dockerfile by removing "image" above and adding this:
#build: .
# Works best when using `docker-compose up --force-recreate --build`.
# Works best when using `docker compose up --force-recreate --build`.
environment:
- CHOKIDAR_USEPOLLING=true
depends_on:
- mongodb
mongodb:

View File

@@ -2,7 +2,7 @@
set -e
if [ ! -f conf/config.json ]; then
jq --arg value "mongodb://mongodb:27017/openWF" '.mongodbUrl = $value' /app/config-vanilla.json > /app/conf/config.json
jq --arg value "mongodb://mongodb:27017/openWF" '.mongodbUrl = $value | del(.bindAddress) | del(.httpPort) | del(.httpsPort)' /app/config-vanilla.json > /app/conf/config.json
fi
exec npm run raw -- --configPath conf/config.json
exec npm run raw -- --configPath conf/config.json --docker

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

@@ -0,0 +1,24 @@
import { getAccountIdForRequest } from "../../services/loginService.ts";
import type { RequestHandler } from "express";
import { getInventory } from "../../services/inventoryService.ts";
export const checkPendingRecipesController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "PendingRecipes");
const now = Date.now();
const resp: ICheckPendingRecipesResponse = {
PendingRecipes: inventory.PendingRecipes.map(recipe => ({
ItemType: recipe.ItemType,
SecondsRemaining: Math.max(0, Math.floor((recipe.CompletionDate.getTime() - now) / 1000))
}))
};
res.send(resp);
};
interface ICheckPendingRecipesResponse {
PendingRecipes: {
ItemType: string;
SecondsRemaining: number;
}[];
}

View File

@@ -22,14 +22,15 @@ import {
import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
import type { IPendingRecipeDatabase } from "../../types/inventoryTypes/inventoryTypes.ts";
import { InventorySlot } from "../../types/inventoryTypes/inventoryTypes.ts";
import { fromOid, toOid2 } from "../../helpers/inventoryHelpers.ts";
import { fromOid, toOid2, version_compare } from "../../helpers/inventoryHelpers.ts";
import type { TInventoryDatabaseDocument } from "../../models/inventoryModels/inventoryModel.ts";
import type { IRecipe } from "warframe-public-export-plus";
import type { IEquipmentClient } from "../../types/equipmentTypes.ts";
import { EquipmentFeatures, Status } from "../../types/equipmentTypes.ts";
interface IClaimCompletedRecipeRequest {
RecipeIds: IOidWithLegacySupport[];
RecipeIds?: IOidWithLegacySupport[]; // U24.4 and beyond
recipeNames?: string[]; // Builds before U24.4 down to U22.20
}
interface IClaimCompletedRecipeResponse {
@@ -38,22 +39,60 @@ interface IClaimCompletedRecipeResponse {
}
export const claimCompletedRecipeController: RequestHandler = async (req, res) => {
const claimCompletedRecipeRequest = getJSONfromString<IClaimCompletedRecipeRequest>(String(req.body));
const account = await getAccountForRequest(req);
const inventory = await getInventory(account._id.toString());
const resp: IClaimCompletedRecipeResponse = {
InventoryChanges: {}
};
for (const recipeId of claimCompletedRecipeRequest.RecipeIds) {
const pendingRecipe = inventory.PendingRecipes.id(fromOid(recipeId));
if (!pendingRecipe) {
throw new Error(`no pending recipe found with id ${fromOid(recipeId)}`);
}
if (!req.query.recipeName) {
const claimCompletedRecipeRequest = getJSONfromString<IClaimCompletedRecipeRequest>(String(req.body));
const recipes = claimCompletedRecipeRequest.recipeNames ?? claimCompletedRecipeRequest.RecipeIds;
if (recipes) {
for (const recipeId of recipes) {
let pendingRecipe;
if (typeof recipeId === "string") {
pendingRecipe = inventory.PendingRecipes.find(r => r.ItemType == recipeId);
if (!pendingRecipe) {
throw new Error(`no pending recipe found of type ${recipeId}`);
}
} else {
pendingRecipe = inventory.PendingRecipes.id(fromOid(recipeId));
if (!pendingRecipe) {
throw new Error(`no pending recipe found with id ${fromOid(recipeId)}`);
}
}
//check recipe is indeed ready to be completed
// if (pendingRecipe.CompletionDate > new Date()) {
// throw new Error(`recipe ${pendingRecipe._id} is not ready to be completed`);
// }
//check recipe is indeed ready to be completed
// if (pendingRecipe.CompletionDate > new Date()) {
// throw new Error(`recipe ${pendingRecipe._id} is not ready to be completed`);
// }
inventory.PendingRecipes.pull(pendingRecipe._id);
const recipe = getRecipe(pendingRecipe.ItemType);
if (!recipe) {
throw new Error(`no completed item found for recipe ${pendingRecipe._id.toString()}`);
}
if (req.query.cancel) {
const inventoryChanges: IInventoryChanges = {};
await refundRecipeIngredients(inventory, inventoryChanges, recipe, pendingRecipe);
await inventory.save();
res.json(inventoryChanges); // Not a bug: In the specific case of cancelling a recipe, InventoryChanges are expected to be the root.
return;
}
await claimCompletedRecipe(account, inventory, recipe, pendingRecipe, resp, req.query.rush);
}
} else {
throw new Error(`recipe list from request was undefined?`);
}
} else {
const recipeName = String(req.query.recipeName); // U8
const pendingRecipe = inventory.PendingRecipes.find(r => r.ItemType == recipeName);
if (!pendingRecipe) {
throw new Error(`no pending recipe found with ItemType ${recipeName}`);
}
inventory.PendingRecipes.pull(pendingRecipe._id);
@@ -61,16 +100,14 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
if (!recipe) {
throw new Error(`no completed item found for recipe ${pendingRecipe._id.toString()}`);
}
if (req.query.cancel) {
const inventoryChanges: IInventoryChanges = {};
await refundRecipeIngredients(inventory, inventoryChanges, recipe, pendingRecipe);
await inventory.save();
res.json(inventoryChanges); // Not a bug: In the specific case of cancelling a recipe, InventoryChanges are expected to be the root.
return;
}
await claimCompletedRecipe(account, inventory, recipe, pendingRecipe, resp, req.query.rush);
await claimCompletedRecipe(
account,
inventory,
recipe,
pendingRecipe,
resp,
req.path.includes("instantCompleteRecipe.php") || req.query.rush
);
}
await inventory.save();
res.json(resp);
@@ -121,13 +158,20 @@ const claimCompletedRecipe = async (
}
if (rush) {
let cost = recipe.skipBuildTimePrice;
const end = Math.trunc(pendingRecipe.CompletionDate.getTime() / 1000);
const start = end - recipe.buildTime;
const secondsElapsed = Math.trunc(Date.now() / 1000) - start;
const progress = secondsElapsed / recipe.buildTime;
logger.debug(`rushing recipe at ${Math.trunc(progress * 100)}% completion`);
const cost =
progress > 0.5 ? Math.round(recipe.skipBuildTimePrice * (1 - (progress - 0.5))) : recipe.skipBuildTimePrice;
// U18 introduced rush cost scaling, don't use it for older versions.
if (account.BuildLabel && version_compare(account.BuildLabel, "2015.12.03.00.00") >= 0) {
// Haven't found the real build label for U18.0.0 yet, but this works
cost =
progress > 0.5
? Math.round(recipe.skipBuildTimePrice * (1 - (progress - 0.5)))
: recipe.skipBuildTimePrice;
}
combineInventoryChanges(resp.InventoryChanges, updateCurrency(inventory, cost, true));
}
@@ -218,9 +262,9 @@ const claimCompletedRecipe = async (
"",
"",
"",
umbraModA.ItemId.$oid,
umbraModB.ItemId.$oid,
umbraModC.ItemId.$oid
fromOid(umbraModA.ItemId),
fromOid(umbraModB.ItemId),
fromOid(umbraModC.ItemId)
]
}
],
@@ -240,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

@@ -25,11 +25,24 @@ export const sellController: RequestHandler = async (req, res) => {
//console.log(JSON.stringify(payload, null, 2));
const accountId = await getAccountIdForRequest(req);
const requiredFields = new Set<keyof TInventoryDatabaseDocument>();
if (payload.SellCurrency == "SC_RegularCredits") {
let sellCurrency = "SC_RegularCredits";
if (payload.SellCurrency) {
sellCurrency = payload.SellCurrency;
} else {
if (payload.SellForFusionPoints || payload.SellForPrimeBucks) {
if (payload.SellForFusionPoints) {
sellCurrency = "SC_FusionPoints";
}
if (payload.SellForPrimeBucks) {
sellCurrency = "SC_PrimeBucks";
}
}
}
if (sellCurrency == "SC_RegularCredits") {
requiredFields.add("RegularCredits");
} else if (payload.SellCurrency == "SC_FusionPoints") {
} else if (sellCurrency == "SC_FusionPoints") {
requiredFields.add("FusionPoints");
} else if (payload.SellCurrency == "SC_CrewShipFusionPoints") {
} else if (sellCurrency == "SC_CrewShipFusionPoints") {
requiredFields.add("CrewShipFusionPoints");
} else {
requiredFields.add("MiscItems");
@@ -83,27 +96,27 @@ export const sellController: RequestHandler = async (req, res) => {
const inventory = await getInventory(accountId, Array.from(requiredFields).join(" "));
// Give currency
if (payload.SellCurrency == "SC_RegularCredits") {
if (sellCurrency == "SC_RegularCredits") {
inventory.RegularCredits += payload.SellPrice;
} else if (payload.SellCurrency == "SC_FusionPoints") {
} else if (sellCurrency == "SC_FusionPoints") {
addFusionPoints(inventory, payload.SellPrice);
} else if (payload.SellCurrency == "SC_CrewShipFusionPoints") {
} else if (sellCurrency == "SC_CrewShipFusionPoints") {
addCrewShipFusionPoints(inventory, payload.SellPrice);
} else if (payload.SellCurrency == "SC_PrimeBucks") {
} else if (sellCurrency == "SC_PrimeBucks") {
addMiscItems(inventory, [
{
ItemType: "/Lotus/Types/Items/MiscItems/PrimeBucks",
ItemCount: payload.SellPrice
}
]);
} else if (payload.SellCurrency == "SC_DistillPoints") {
} else if (sellCurrency == "SC_DistillPoints") {
addMiscItems(inventory, [
{
ItemType: "/Lotus/Types/Items/MiscItems/DistillPoints",
ItemCount: payload.SellPrice
}
]);
} else if (payload.SellCurrency == "SC_Resources") {
} else if (sellCurrency == "SC_Resources") {
// Will add appropriate MiscItems from CrewShipWeapons or CrewShipWeaponSkins
} else {
throw new Error("Unknown SellCurrency: " + payload.SellCurrency);
@@ -218,7 +231,7 @@ export const sellController: RequestHandler = async (req, res) => {
} else {
const index = inventory.CrewShipWeapons.findIndex(x => x._id.equals(sellItem.String));
if (index != -1) {
if (payload.SellCurrency == "SC_Resources") {
if (sellCurrency == "SC_Resources") {
refundPartialBuildCosts(inventory, inventory.CrewShipWeapons[index].ItemType, inventoryChanges);
}
inventory.CrewShipWeapons.splice(index, 1);
@@ -241,7 +254,7 @@ export const sellController: RequestHandler = async (req, res) => {
} else {
const index = inventory.CrewShipWeaponSkins.findIndex(x => x._id.equals(sellItem.String));
if (index != -1) {
if (payload.SellCurrency == "SC_Resources") {
if (sellCurrency == "SC_Resources") {
refundPartialBuildCosts(
inventory,
inventory.CrewShipWeaponSkins[index].ItemType,
@@ -346,7 +359,7 @@ interface ISellRequest {
WeaponSkins?: ISellItem[]; // SNS specific field
};
SellPrice: number;
SellCurrency:
SellCurrency?:
| "SC_RegularCredits"
| "SC_PrimeBucks"
| "SC_FusionPoints"
@@ -355,6 +368,9 @@ interface ISellRequest {
| "SC_Resources"
| "somethingelsewemightnotknowabout";
buildLabel: string;
// These are used in old builds (undetermined where it changed) instead of SellCurrency
SellForPrimeBucks?: boolean;
SellForFusionPoints?: boolean;
}
interface ISellItem {

View File

@@ -24,7 +24,8 @@ export const startRecipeController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const recipeName = startRecipeRequest.RecipeName;
let recipeName = startRecipeRequest.RecipeName;
if (req.query.recipeName) recipeName = String(req.query.recipeName); // U8
const recipe = getRecipe(recipeName);
if (!recipe) {
@@ -68,7 +69,9 @@ export const startRecipeController: RequestHandler = async (req, res) => {
freeUpSlot(inventory, InventorySlot.WEAPONS);
}
} else {
await addItem(inventory, recipe.ingredients[i].ItemType, recipe.ingredients[i].ItemCount * -1);
const itemType = recipe.ingredients[i].ItemType;
const itemCount = recipe.ingredients[i].ItemCount;
await addItem(inventory, itemType, itemCount * -1);
}
}

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

@@ -35,6 +35,7 @@ export const completeAllMissionsController: RequestHandler = async (req, res) =>
await handleStoreItemAcquisition(reward.StoreItem, inventory, reward.ItemCount, undefined, true);
}
addString(inventory.NodeIntrosCompleted, "TeshinHardModeUnlocked");
addString(inventory.NodeIntrosCompleted, "CetusInvasionNodeIntro");
addString(inventory.NodeIntrosCompleted, "CetusSyndicate_IntroJob");
let syndicate = inventory.Affiliations.find(x => x.Tag == "CetusSyndicate");
if (!syndicate) {

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

@@ -0,0 +1,22 @@
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { getInventory } from "../../services/inventoryService.ts";
import type { RequestHandler } from "express";
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
export const setUmbraEchoesController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const request = req.body as ISetUmbraEchoesRequest;
const inventory = await getInventory(accountId, "Suits");
const suit = inventory.Suits.id(request.oid);
if (suit) {
suit.UmbraDate = request.UmbraDate ? new Date(request.UmbraDate) : undefined;
await inventory.save();
broadcastInventoryUpdate(req);
}
res.end();
};
interface ISetUmbraEchoesRequest {
oid: string;
UmbraDate: number;
}

View File

@@ -3,10 +3,11 @@ import allScans from "../../../static/fixed_responses/allScans.json" with { type
import { ExportEnemies } from "warframe-public-export-plus";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { getStats } from "../../services/statsService.ts";
import { getInventory } from "../../services/inventoryService.ts";
export const unlockAllScansController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const stats = await getStats(accountId);
const [stats, inventory] = await Promise.all([getStats(accountId), getInventory(accountId, "ChallengeProgress")]);
const scanTypes = new Set<string>(allScans);
for (const type of Object.keys(ExportEnemies.avatars)) {
@@ -18,6 +19,17 @@ export const unlockAllScansController: RequestHandler = async (req, res) => {
stats.Scans.push({ type, scans: 9999 });
}
await stats.save();
const jsCodex = inventory.ChallengeProgress.find(x => x.Name === "JSCodexScan");
if (jsCodex) {
jsCodex.Progress = 1;
} else {
inventory.ChallengeProgress.push({
Name: "JSCodexScan",
Progress: 1
});
}
await Promise.all([stats.save(), inventory.save()]);
res.end();
};

View File

@@ -1,7 +1,7 @@
import { getLeaderboard } from "../../services/leaderboardService.ts";
import type { RequestHandler } from "express";
export const leaderboardController: RequestHandler = async (req, res) => {
export const leaderboardPostController: RequestHandler = async (req, res) => {
const payload = JSON.parse(String(req.body)) as ILeaderboardRequest;
res.json({
results: await getLeaderboard(
@@ -15,6 +15,33 @@ export const leaderboardController: RequestHandler = async (req, res) => {
});
};
export const leaderboardGetController: RequestHandler = async (req, res) => {
const payload: ILeaderboardRequest = {
field: "archived." + String(req.query.field),
before: Number(req.query.before),
after: Number(req.query.after),
pivotId: req.query.pivotAccountId ? String(req.query.pivotAccountId) : undefined,
guildId: undefined,
guildTier: undefined
};
res.json({
players: (
await getLeaderboard(
payload.field,
payload.before,
payload.after,
payload.pivotId,
payload.guildId,
payload.guildTier
)
).map(entry => ({
DisplayName: entry.n,
score: entry.s,
rank: entry.r
}))
});
};
interface ILeaderboardRequest {
field: string;
before: number;

View File

@@ -2,6 +2,7 @@ interface IArguments {
configPath?: string;
dev?: boolean;
secret?: string;
docker?: boolean;
}
export const args: IArguments = {};
@@ -19,5 +20,9 @@ for (let i = 2; i < process.argv.length; ) {
case "--secret":
args.secret = process.argv[i++];
break;
case "--docker":
args.docker = true;
break;
}
}

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

@@ -20,6 +20,7 @@ import { cancelGuildAdvertisementController } from "../controllers/api/cancelGui
import { changeDojoRootController } from "../controllers/api/changeDojoRootController.ts";
import { changeGuildRankController } from "../controllers/api/changeGuildRankController.ts";
import { checkDailyMissionBonusController } from "../controllers/api/checkDailyMissionBonusController.ts";
import { checkPendingRecipesController } from "../controllers/api/checkPendingRecipesController.ts";
import { claimCompletedRecipeController } from "../controllers/api/claimCompletedRecipeController.ts";
import { claimJunctionChallengeRewardController } from "../controllers/api/claimJunctionChallengeRewardController.ts";
import { claimLibraryDailyTaskRewardController } from "../controllers/api/claimLibraryDailyTaskRewardController.ts";
@@ -184,6 +185,7 @@ apiRouter.get("/cancelGuildAdvertisement.php", cancelGuildAdvertisementControlle
apiRouter.get("/changeDojoRoot.php", changeDojoRootController);
apiRouter.get("/changeGuildRank.php", changeGuildRankController);
apiRouter.get("/checkDailyMissionBonus.php", checkDailyMissionBonusController);
apiRouter.get("/checkPendingRecipes.php", checkPendingRecipesController); // U8
apiRouter.get("/claimLibraryDailyTaskReward.php", claimLibraryDailyTaskRewardController);
apiRouter.get("/completeCalendarEvent.php", completeCalendarEventController);
apiRouter.get("/confirmAllianceInvitation.php", confirmAllianceInvitationController);
@@ -305,6 +307,7 @@ apiRouter.post("/guildTech.php", guildTechController);
apiRouter.post("/hostSession.php", hostSessionController);
apiRouter.post("/hubBlessing.php", hubBlessingController);
apiRouter.post("/infestedFoundry.php", infestedFoundryController);
apiRouter.post("/instantCompleteRecipe.php", claimCompletedRecipeController); // U8
apiRouter.post("/inventorySlots.php", inventorySlotsController);
apiRouter.post("/joinSession.php", joinSessionController);
apiRouter.post("/login.php", loginController);

View File

@@ -46,6 +46,7 @@ import { updateFingerprintController } from "../controllers/custom/updateFingerp
import { unlockLevelCapController } from "../controllers/custom/unlockLevelCapController.ts";
import { changeModularPartsController } from "../controllers/custom/changeModularPartsController.ts";
import { setInvigorationController } from "../controllers/custom/setInvigorationController.ts";
import { setUmbraEchoesController } from "../controllers/custom/setUmbraEchoesController.ts";
import { setAccountCheatController } from "../controllers/custom/setAccountCheatController.ts";
import { setGuildCheatController } from "../controllers/custom/setGuildCheatController.ts";
@@ -97,6 +98,7 @@ customRouter.post("/updateFingerprint", updateFingerprintController);
customRouter.post("/unlockLevelCap", unlockLevelCapController);
customRouter.post("/changeModularParts", changeModularPartsController);
customRouter.post("/setInvigoration", setInvigorationController);
customRouter.post("/setUmbraEchoes", setUmbraEchoesController);
customRouter.post("/setAccountCheat", setAccountCheatController);
customRouter.post("/setGuildCheat", setGuildCheatController);

View File

@@ -1,14 +1,16 @@
import express from "express";
import { viewController } from "../controllers/stats/viewController.ts";
import { uploadController } from "../controllers/stats/uploadController.ts";
import { leaderboardController } from "../controllers/stats/leaderboardController.ts";
import { leaderboardPostController, leaderboardGetController } from "../controllers/stats/leaderboardController.ts";
const statsRouter = express.Router();
statsRouter.get("/view.php", viewController);
statsRouter.get("/profileStats.php", viewController);
statsRouter.get("/leaderboard.php", leaderboardGetController);
statsRouter.post("/upload.php", uploadController);
statsRouter.post("/leaderboardWeekly.php", leaderboardController);
statsRouter.post("/leaderboardArchived.php", leaderboardController);
statsRouter.post("/view.php", viewController);
statsRouter.post("/leaderboardWeekly.php", leaderboardPostController);
statsRouter.post("/leaderboardArchived.php", leaderboardPostController);
export { statsRouter };

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

@@ -160,6 +160,11 @@ export const configRemovedOptionsKeys = [
"relicRewardItemCountMultiplier",
"nightwaveStandingMultiplier"
];
if (args.docker) {
configRemovedOptionsKeys.push("bindAddress");
configRemovedOptionsKeys.push("httpPort");
configRemovedOptionsKeys.push("httpsPort");
}
export const configPath = path.join(repoDir, args.configPath ?? "config.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": {
@@ -808,13 +863,24 @@ export const addItem = async (
typeName.substring(1).split("/")[3] == "KubrowPet"
) {
if (
typeName != "/Lotus/Types/Game/KubrowPet/Eggs/KubrowEgg" &&
typeName != "/Lotus/Types/Game/KubrowPet/Eggs/KubrowPetEggItem" &&
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`);
}
@@ -824,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;
@@ -839,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>()]
@@ -846,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",
@@ -884,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,
@@ -921,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) {
@@ -1848,8 +1940,10 @@ export const addMiscItems = (inventory: TInventoryDatabaseDocument, itemsArray:
if (MiscItems[itemIndex].ItemCount == 0) {
MiscItems.splice(itemIndex, 1);
} else if (MiscItems[itemIndex].ItemCount <= 0) {
logger.warn(`inventory.MiscItems has a negative count for ${ItemType}`);
} else if (MiscItems[itemIndex].ItemCount < 0) {
throw new Error(
`Cannot remove ${ItemCount * -1}x ${ItemType} from MiscItems, would be left with ${MiscItems[itemIndex].ItemCount}`
);
}
});
};
@@ -1870,8 +1964,10 @@ const applyArrayChanges = (
arr[itemIndex].ItemCount += change.ItemCount;
if (arr[itemIndex].ItemCount == 0) {
arr.splice(itemIndex, 1);
} else if (arr[itemIndex].ItemCount <= 0) {
logger.warn(`inventory.${key} has a negative count for ${change.ItemType}`);
} else if (arr[itemIndex].ItemCount < 0) {
throw new Error(
`Cannot remove ${change.ItemCount * -1}x ${change.ItemType} from ${key}, would be left with ${arr[itemIndex].ItemCount}`
);
}
}
}
@@ -1917,8 +2013,10 @@ export const addMods = (inventory: TInventoryDatabaseDocument, itemsArray: IRawU
RawUpgrades[itemIndex].ItemCount += ItemCount;
if (RawUpgrades[itemIndex].ItemCount == 0) {
RawUpgrades.splice(itemIndex, 1);
} else if (RawUpgrades[itemIndex].ItemCount <= 0) {
logger.warn(`inventory.RawUpgrades has a negative count for ${ItemType}`);
} else if (RawUpgrades[itemIndex].ItemCount < 0) {
throw new Error(
`Cannot remove ${ItemCount * -1}x ${ItemType} from RawUpgrades, would be left with ${RawUpgrades[itemIndex].ItemCount}`
);
}
});
};
@@ -1933,7 +2031,9 @@ export const addFusionTreasures = (inventory: TInventoryDatabaseDocument, itemsA
if (FusionTreasures[itemIndex].ItemCount == 0) {
FusionTreasures.splice(itemIndex, 1);
} else if (FusionTreasures[itemIndex].ItemCount <= 0) {
logger.warn(`inventory.FusionTreasures has a negative count for ${ItemType}`);
throw new Error(
`Cannot remove ${ItemCount * -1}x ${ItemType} from FusionTreasures, would be left with ${FusionTreasures[itemIndex].ItemCount}`
);
}
} else {
FusionTreasures.push({ ItemCount, ItemType, Sockets });

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",
@@ -3819,6 +3824,12 @@ export const getNightwaveSyndicateTag = (buildLabel: string | undefined): string
if (version_compare(buildLabel, "2025.02.05.11.19") >= 0) {
return "RadioLegionIntermission12Syndicate";
}
if (version_compare(buildLabel, "2024.08.21.20.02") >= 0) {
return "RadioLegionIntermission11Syndicate";
}
if (version_compare(buildLabel, "2024.04.29.11.14") >= 0) {
return "RadioLegionIntermission10Syndicate";
}
return undefined;
};

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

@@ -822,6 +822,22 @@
</form>
</div>
</div>
<div id="umbraEchoes-card" class="card mb-3 d-none">
<h5 class="card-header" data-loc="detailedView_umbraEchoesLabel"></h5>
<div class="card-body">
<p data-loc="detailedView_umbraEchoesDescription"></p>
<form onsubmit="submitUmbraEchoes(event)">
<div class="mb-3">
<label for="umbraEchoes-expiry" class="form-label" data-loc="detailedView_umbraEchoesExpiryLabel"></label>
<input type="datetime-local" class="form-control" max="2038-01-19T03:14" id="umbraEchoes-expiry" onblur="this.value=new Date(this.value)>new Date(this.max)?new Date(this.max).toISOString().slice(0,16):this.value"/>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary" data-loc="general_setButton"></button>
<button type="button" class="btn btn-danger" onclick="setUmbraEchoes()" data-loc="code_remove"></button>
</div>
</form>
</div>
</div>
<div id="modularParts-card" class="card mb-3 d-none">
<h5 class="card-header" data-loc="detailedView_modularPartsLabel"></h5>
<div class="card-body">
@@ -944,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>
@@ -1542,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",
@@ -1507,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>";
@@ -1665,6 +1683,12 @@ function updateInventory() {
formatDatetime("%Y-%m-%d %H:%M", Number(item.UpgradesExpiry?.$date.$numberLong)) || "";
}
if (item.ItemType != "/Lotus/Powersuits/Excalibur/ExcaliburUmbra") {
document.getElementById("umbraEchoes-card").classList.remove("d-none");
document.getElementById("umbraEchoes-expiry").value =
formatDatetime("%Y-%m-%d %H:%M", Number(item.UmbraDate?.$date.$numberLong)) || "";
}
{
document.getElementById("loadout-card").classList.remove("d-none");
const maxModConfigNum = Math.min(2 + (item.ModSlotPurchases ?? 0), 5);
@@ -3457,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(() => {
@@ -3532,6 +3559,7 @@ single.getRoute("#detailedView-route").on("beforeload", function () {
document.getElementById("loadout-card").classList.add("d-none");
document.getElementById("archonShards-card").classList.add("d-none");
document.getElementById("edit-suit-invigorations-card").classList.add("d-none");
document.getElementById("umbraEchoes-card").classList.add("d-none");
document.getElementById("modularParts-card").classList.add("d-none");
document.getElementById("modularParts-form").innerHTML = "";
document.getElementById("valenceBonus-card").classList.add("d-none");
@@ -3618,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) {
@@ -4119,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) {
@@ -4270,6 +4318,25 @@ function setInvigoration(data) {
});
}
function submitUmbraEchoes(event) {
event.preventDefault();
const expiry = document.getElementById("umbraEchoes-expiry").value;
setUmbraEchoes({
UmbraDate: expiry ? new Date(expiry).getTime() : Date.now() + 1 * 24 * 60 * 60 * 1000
});
}
function setUmbraEchoes(data) {
const oid = new URLSearchParams(window.location.search).get("itemId");
$.post({
url: "/custom/setUmbraEchoes?" + window.authz,
contentType: "application/json",
data: JSON.stringify({ oid, ...data })
}).done(function () {
updateInventory();
});
}
function handleAbilityOverride(event, configIndex) {
event.preventDefault();
const urlParams = new URLSearchParams(window.location.search);

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)`,
@@ -80,7 +83,7 @@ dict = {
code_drifterFaceName: `Drifter-Gesicht: |INDEX|`,
code_operatorFaceName: `Operator-Gesicht: |INDEX|`,
code_reviveBooster: `Wiederbelebungsbooster`,
code_reviveBoosterDesc: `[UNTRANSLATED] Set revive count to 4. Disable self-revive restriction on Archon Hunt missions.`,
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`,
@@ -165,6 +168,7 @@ dict = {
detailedView_invigorationLabel: `Kräftigung`,
detailedView_loadoutLabel: `Loadouts`,
detailedView_equipmentFeaturesLabel: `[UNTRANSLATED] Equipment Features`,
detailedView_umbraEchoesLabel: `[UNTRANSLATED] Echoes Of Umbra`,
invigorations_offensive_AbilityStrength: `+200% Fähigkeitsstärke`,
invigorations_offensive_AbilityRange: `+100% Fähigkeitsreichweite`,
@@ -195,6 +199,9 @@ dict = {
abilityOverride_label: `Fähigkeitsüberschreibung`,
abilityOverride_onSlot: `auf Slot`,
detailedView_umbraEchoesDescription: `Wird ein Warframe mit dieser Flüssigkeit injiziert, kann er selbstständig an der Seite des Operators kämpfen.`,
detailedView_umbraEchoesExpiryLabel: `[UNTRANSLATED] Echo Expiry (optional)`,
mods_addRiven: `Riven hinzufügen`,
mods_fingerprint: `Fingerabdruck`,
mods_fingerprintHelp: `Benötigst du Hilfe mit dem Fingerabdruck?`,
@@ -208,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`,
@@ -366,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)`,
@@ -79,7 +82,7 @@ dict = {
code_drifterFaceName: `Drifter Visage |INDEX|`,
code_operatorFaceName: `Operator Visage |INDEX|`,
code_reviveBooster: `Revive Booster`,
code_reviveBoosterDesc: `Set revive count to 4. Disable self-revive restriction on Archon Hunt missions.`,
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`,
@@ -164,6 +167,7 @@ dict = {
detailedView_invigorationLabel: `Invigoration`,
detailedView_loadoutLabel: `Loadouts`,
detailedView_equipmentFeaturesLabel: `Equipment Features`,
detailedView_umbraEchoesLabel: `Echoes Of Umbra`,
invigorations_offensive_AbilityStrength: `+200% Ability Strength`,
invigorations_offensive_AbilityRange: `+100% Ability Range`,
@@ -194,6 +198,9 @@ dict = {
abilityOverride_label: `Ability Override`,
abilityOverride_onSlot: `on slot`,
detailedView_umbraEchoesDescription: `Injecting this fluid into a Warframe will imbue it with the ability to fight autonomously alongside the Operator.`,
detailedView_umbraEchoesExpiryLabel: `Echo Expiry (optional)`,
mods_addRiven: `Add Riven`,
mods_fingerprint: `Fingerprint`,
mods_fingerprintHelp: `Need help with the fingerprint?`,
@@ -207,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`,
@@ -365,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)`,
@@ -80,7 +83,7 @@ dict = {
code_drifterFaceName: `Rostro del Viajero |INDEX|`,
code_operatorFaceName: `Rostro del operador |INDEX|`,
code_reviveBooster: `Potenciador de reanimaciones`,
code_reviveBoosterDesc: `[UNTRANSLATED] Set revive count to 4. Disable self-revive restriction on Archon Hunt missions.`,
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`,
@@ -165,6 +168,7 @@ dict = {
detailedView_invigorationLabel: `Fortalecimiento`,
detailedView_loadoutLabel: `Equipamientos`,
detailedView_equipmentFeaturesLabel: `[UNTRANSLATED] Equipment Features`,
detailedView_umbraEchoesLabel: `[UNTRANSLATED] Echoes Of Umbra`,
invigorations_offensive_AbilityStrength: `+200% Fuerza de Habilidad`,
invigorations_offensive_AbilityRange: `+100% Alcance de Habilidad`,
@@ -195,6 +199,9 @@ dict = {
abilityOverride_label: `Intercambio de Habilidad`,
abilityOverride_onSlot: `en el espacio`,
detailedView_umbraEchoesDescription: `Inyectar este fluido en un warframe lo imbuirá con la capacidad para luchar autónomamente junto a su operador.`,
detailedView_umbraEchoesExpiryLabel: `[UNTRANSLATED] Echo Expiry (optional)`,
mods_addRiven: `Agregar Agrietado`,
mods_fingerprint: `Huella digital`,
mods_fingerprintHelp: `¿Necesitas ayuda con la huella digital?`,
@@ -208,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`,
@@ -366,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)`,
@@ -80,7 +83,7 @@ dict = {
code_drifterFaceName: `Visage du Voyageur |INDEX|`,
code_operatorFaceName: `Visage de l'Opérateur |INDEX|`,
code_reviveBooster: `Booster de Réanimation`,
code_reviveBoosterDesc: `[UNTRANSLATED] Set revive count to 4. Disable self-revive restriction on Archon Hunt missions.`,
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`,
@@ -165,6 +168,7 @@ dict = {
detailedView_invigorationLabel: `Dynamisation`,
detailedView_loadoutLabel: `Équipements`,
detailedView_equipmentFeaturesLabel: `[UNTRANSLATED] Equipment Features`,
detailedView_umbraEchoesLabel: `[UNTRANSLATED] Echoes Of Umbra`,
invigorations_offensive_AbilityStrength: `+200% de puissance de pouvoir`,
invigorations_offensive_AbilityRange: `+100% de portée de pouvoir`,
@@ -195,6 +199,9 @@ dict = {
abilityOverride_label: `Remplacement de pouvoir`,
abilityOverride_onSlot: `Sur l'emplacement`,
detailedView_umbraEchoesDescription: `L'injection de ce fluide dans une Warframe lui donnera la possibilité de se battre de manière autonome aux côtés de l'Opérateur.`,
detailedView_umbraEchoesExpiryLabel: `[UNTRANSLATED] Echo Expiry (optional)`,
mods_addRiven: `Ajouter un riven`,
mods_fingerprint: `Empreinte`,
mods_fingerprintHelp: `Besoin d'aide pour l'empreinte ?`,
@@ -208,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`,
@@ -366,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| (Повреждённый)`,
@@ -165,6 +168,7 @@ dict = {
detailedView_invigorationLabel: `Воодушевление`,
detailedView_loadoutLabel: `Конфигурации`,
detailedView_equipmentFeaturesLabel: `Модификаторы снаряжения`,
detailedView_umbraEchoesLabel: `Эхо Умбры`,
invigorations_offensive_AbilityStrength: `+200% к силе способностей.`,
invigorations_offensive_AbilityRange: `+100% к зоне поражения способностей.`,
@@ -195,6 +199,9 @@ dict = {
abilityOverride_label: `Переопределение способности`,
abilityOverride_onSlot: `в ячейке`,
detailedView_umbraEchoesDescription: `Введение этой жидкости в варфрейм позволит ему автономно сражаться бок о бок с оператором.`,
detailedView_umbraEchoesExpiryLabel: `Срок действия Эха (необязательно)`,
mods_addRiven: `Добавить мод Разлома`,
mods_fingerprint: `Отпечаток`,
mods_fingerprintHelp: `Нужна помощь с отпечатком?`,
@@ -208,6 +215,7 @@ dict = {
cheats_server: `Сервер`,
cheats_skipTutorial: `Пропустить обучение`,
cheats_skipAllDialogue: `Пропустить все диалоги`,
cheats_skipAllPopups: `[UNTRANSLATED] Skip All Popups`,
cheats_unlockAllScans: `Разблокировать все сканирования`,
cheats_unlockSuccRelog: `Успех. Вам необходимо повторно войти в игру, чтобы клиент обновил эту информацию.`,
cheats_unlockAllMissions: `Разблокировать все миссии`,
@@ -366,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| (Пошкоджений)`,
@@ -80,7 +83,7 @@ dict = {
code_drifterFaceName: `Зовнішність мандрівника: |INDEX|`,
code_operatorFaceName: `Зовнішність оператора: |INDEX|`,
code_reviveBooster: `Збільшувач зцілення`,
code_reviveBoosterDesc: `[UNTRANSLATED] Set revive count to 4. Disable self-revive restriction on Archon Hunt missions.`,
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`,
@@ -165,6 +168,7 @@ dict = {
detailedView_invigorationLabel: `Зміцнення`,
detailedView_loadoutLabel: `Конфігурації`,
detailedView_equipmentFeaturesLabel: `[UNTRANSLATED] Equipment Features`,
detailedView_umbraEchoesLabel: `[UNTRANSLATED] Echoes Of Umbra`,
invigorations_offensive_AbilityStrength: `+200% до потужності здібностей.`,
invigorations_offensive_AbilityRange: `+100% до досяжності здібностей.`,
@@ -195,6 +199,9 @@ dict = {
abilityOverride_label: `Перевизначення здібностей`,
abilityOverride_onSlot: `у комірці`,
detailedView_umbraEchoesDescription: `Рідина, яка після введення дозволяє ворфрейму використовувати єдність і битися самостійно пліч-о-пліч з оператором.`,
detailedView_umbraEchoesExpiryLabel: `[UNTRANSLATED] Echo Expiry (optional)`,
mods_addRiven: `Добавити модифікатор Розколу`,
mods_fingerprint: `Відбиток`,
mods_fingerprintHelp: `Потрібна допомога з відбитком?`,
@@ -208,6 +215,7 @@ dict = {
cheats_server: `Сервер`,
cheats_skipTutorial: `Пропустити навчання`,
cheats_skipAllDialogue: `Пропустити всі діалоги`,
cheats_skipAllPopups: `[UNTRANSLATED] Skip All Popups`,
cheats_unlockAllScans: `Розблокувати всі сканування`,
cheats_unlockSuccRelog: `Успіх. Вам потрібно буде повторно увійти в гру, щоб клієнт оновив цю інформацію.`,
cheats_unlockAllMissions: `Розблокувати всі місії`,
@@ -366,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| (有瑕疵的)`,
@@ -80,7 +83,7 @@ dict = {
code_drifterFaceName: `漂泊者面部 |INDEX|`,
code_operatorFaceName: `指挥官面部 |INDEX|`,
code_reviveBooster: `复活加速器`,
code_reviveBoosterDesc: `[UNTRANSLATED] Set revive count to 4. Disable self-revive restriction on Archon Hunt missions.`,
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`,
@@ -165,6 +168,7 @@ dict = {
detailedView_invigorationLabel: `活化`,
detailedView_loadoutLabel: `配置`,
detailedView_equipmentFeaturesLabel: `[UNTRANSLATED] Equipment Features`,
detailedView_umbraEchoesLabel: `[UNTRANSLATED] Echoes Of Umbra`,
invigorations_offensive_AbilityStrength: `+200%技能强度`,
invigorations_offensive_AbilityRange: `+100%技能范围`,
@@ -195,6 +199,9 @@ dict = {
abilityOverride_label: `技能替换`,
abilityOverride_onSlot: `槽位`,
detailedView_umbraEchoesDescription: `将这种液体注入战甲内,使其具有与指挥官并肩作战的能力。`,
detailedView_umbraEchoesExpiryLabel: `[UNTRANSLATED] Echo Expiry (optional)`,
mods_addRiven: `添加裂罅MOD`,
mods_fingerprint: `印记`,
mods_fingerprintHelp: `需要印记相关的帮助?`,
@@ -208,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: `解锁所有星图`,
@@ -366,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,