Compare commits

...

129 Commits
main ... main

Author SHA1 Message Date
c283d61399 chore(webui): update to Spanish translation (#2165)
Reviewed-on: OpenWF/SpaceNinjaServer#2165
Co-authored-by: hxedcl <hxedcl@noreply.localhost>
Co-committed-by: hxedcl <hxedcl@noreply.localhost>
2025-06-14 23:44:34 -07:00
12d09531b3 feat: add dev.keepVendorsExpired config option (#2161)
Reviewed-on: OpenWF/SpaceNinjaServer#2161
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-14 12:58:26 -07:00
b67ddf6df2 feat: handle classic syndicate alignments when trading in medals (#2157)
Reviewed-on: OpenWF/SpaceNinjaServer#2157
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-14 12:57:40 -07:00
8b27fcf459 chore(webui): add archon crystal upgrades to translation system (#2154)
Reviewed-on: OpenWF/SpaceNinjaServer#2154
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-13 19:57:19 -07:00
071ef528ea fix: syndicate rank up from negative levels (#2156)
For level <= 0, SacrificeLevel is the current level.

Reviewed-on: OpenWF/SpaceNinjaServer#2156
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-13 10:58:41 -07:00
71d1b6094c feat: randomise classic bounty xpAmounts (#2150)
Reviewed-on: OpenWF/SpaceNinjaServer#2150
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-13 04:46:18 -07:00
fcc11206cc fix: multiple syndicate level ups (#2152)
Regression from 54a73ad5d7eab867a1701ccf66d56446db96c226 because I forgot that levelIncrease could now be >1

Reviewed-on: OpenWF/SpaceNinjaServer#2152
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-13 04:46:04 -07:00
3c019e41b9 chore(webui): update to Spanish translation (#2151)
Reviewed-on: OpenWF/SpaceNinjaServer#2151
Co-authored-by: hxedcl <hxedcl@noreply.localhost>
Co-committed-by: hxedcl <hxedcl@noreply.localhost>
2025-06-12 13:23:51 -07:00
54a73ad5d7 fix: syndicate initiation (#2149)
Was accidentially broken by 1979b20f8cc00b79f305478f1da3d69f90a3d43e

Reviewed-on: OpenWF/SpaceNinjaServer#2149
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-12 09:20:32 -07:00
62eeb313ec feat: nightwaveStandingMultiplier cheat (#2145)
Co-authored-by: nyaoouo <64143453+nyaoouo@users.noreply.github.com>
Co-authored-by: ny <64143453+nyaoouo@users.noreply.github.com>
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Reviewed-on: OpenWF/SpaceNinjaServer#2145
Co-authored-by: nyaoouo <nyaoouo@noreply.localhost>
Co-committed-by: nyaoouo <nyaoouo@noreply.localhost>
2025-06-12 04:54:41 -07:00
62d4b9f6cb feat(webui): boosters (#2140)
Co-authored-by: ny <64143453+nyaoouo@users.noreply.github.com>
Co-authored-by: nyaoouo <64143453+nyaoouo@users.noreply.github.com>
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Reviewed-on: OpenWF/SpaceNinjaServer#2140
Co-authored-by: nyaoouo <nyaoouo@noreply.localhost>
Co-committed-by: nyaoouo <nyaoouo@noreply.localhost>
2025-06-12 04:54:17 -07:00
1d813a1b1b fix: toStoreItem not converting boosters (#2147)
Bug reported via #2146

Reviewed-on: OpenWF/SpaceNinjaServer#2147
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-11 03:10:10 -07:00
4823406229 chore: don't fail missionInventoryUpdate on unknown items (#2144)
It's possible we started a mission, deleted an item in the webui, and then finished it. The mission completion is still valid, we just can't update that item.

Reviewed-on: OpenWF/SpaceNinjaServer#2144
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-11 02:32:51 -07:00
bdc41de8bb fix: set incubated pet as in stasis when one is already active (#2143)
Previously was kept in incubating state which is obviously wrong

Reviewed-on: OpenWF/SpaceNinjaServer#2143
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-11 02:31:14 -07:00
60236a1154 chore: update PE+ (#2142)
Closes #2141

Reviewed-on: OpenWF/SpaceNinjaServer#2142
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-10 09:48:13 -07:00
1979b20f8c fix: handle multiple syndicate title increases at once (#2139)
Only really possible with nightwave afaik. Bug reported via #2138.

Reviewed-on: OpenWF/SpaceNinjaServer#2139
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-10 05:49:43 -07:00
c736310ff3 feat: send clan search message when reaching MR 2 (#2136)
Closes #1960

Reviewed-on: OpenWF/SpaceNinjaServer#2136
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-09 11:03:56 -07:00
2b555a6456 chore(webui): update Chinese translation (#2137)
Reviewed-on: OpenWF/SpaceNinjaServer#2137
Co-authored-by: bishan178 <bishan178@noreply.localhost>
Co-committed-by: bishan178 <bishan178@noreply.localhost>
2025-06-09 08:20:34 -07:00
870c964854 feat: add eidolonOverride & vallisOverride to replace lockTime (#2135)
I think for now it's best to keep the client time somewhat in sync with the server/database time to avoid various issues.

Reviewed-on: OpenWF/SpaceNinjaServer#2135
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-09 06:54:58 -07:00
4535b193e0 chore: handle nightwaveOverride having an invalid value (#2133)
Reviewed-on: OpenWF/SpaceNinjaServer#2133
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-09 03:37:42 -07:00
943574bf3a chore(webui): update Chinese translation (#2134)
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Reviewed-on: OpenWF/SpaceNinjaServer#2134
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-06-08 11:14:28 -07:00
f9a4d48b4d chore: base all cycles on locked time if used (#2128)
Reviewed-on: OpenWF/SpaceNinjaServer#2128
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-07 16:46:45 -07:00
135b1e54fe feat: classic lich guess history (#2129)
Closes #2123

Reviewed-on: OpenWF/SpaceNinjaServer#2129
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-07 16:46:18 -07:00
b7c47b91ff chore: improve getItemCategoryByUniqueName (#2130)
unused function, but might as well make it at least half decent

Reviewed-on: OpenWF/SpaceNinjaServer#2130
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-07 16:46:00 -07:00
9def5c265e feat: kubrow & kavat incubation (#2131)
Closes #377

Reviewed-on: OpenWF/SpaceNinjaServer#2131
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-07 16:45:50 -07:00
65387ccdea feat(webui): disambiguate gear and resource with the same name for add items (#2127)
Closes #2097

Reviewed-on: OpenWF/SpaceNinjaServer#2127
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-07 02:17:27 -07:00
4118528603 chore: some minor improvements to nemesis mode=s (#2126)
Reviewed-on: OpenWF/SpaceNinjaServer#2126
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-07 02:17:05 -07:00
8ffbb308c5 fix: oull being considered an incorrect guess (#2125)
Closes #2121

Reviewed-on: OpenWF/SpaceNinjaServer#2125
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-07 02:16:50 -07:00
2e649cabf6 chore: handle purchasing decree from acrithis (#2124)
Closes #2112

Reviewed-on: OpenWF/SpaceNinjaServer#2124
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-07 02:16:33 -07:00
5c5296d565 feat: add nightwaveOverride to config (#2120)
Reviewed-on: OpenWF/SpaceNinjaServer#2120
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-07 02:16:19 -07:00
8f5f2fc206 chore: handle numbers in config administratorNames (#2117)
Reviewed-on: OpenWF/SpaceNinjaServer#2117
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-07 02:15:35 -07:00
0997f9567f fix: cap nemesis rank (#2122)
Re #2121

Reviewed-on: OpenWF/SpaceNinjaServer#2122
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-06 16:59:13 -07:00
be02435661 chore: handle addItem of /Lotus/Types/Items/Emotes/** based on path (#2116)
Closes #2114

Reviewed-on: OpenWF/SpaceNinjaServer#2116
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-06 16:29:25 -07:00
20c4092dfe fix: swapped operands (#2119)
Closes #2118

Reviewed-on: OpenWF/SpaceNinjaServer#2119
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-06 13:13:36 -07:00
01492f4f16 fix: swapped operands (#2115)
Closes #2113

Reviewed-on: OpenWF/SpaceNinjaServer#2115
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-06 08:59:22 -07:00
d739945a1d fix: check that syndicateMissionId is not undefined (#2110)
Closes #2111

Reviewed-on: OpenWF/SpaceNinjaServer#2110
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-03 10:40:28 -07:00
d43e39d7b5 fix(webui): error when an unknown suit is max rank (#2109)
Reviewed-on: OpenWF/SpaceNinjaServer#2109
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-02 01:44:10 -07:00
b0499a62aa chore: use 'git checkout -f' instead of 'git reset --hard' to avoid loss 2025-06-01 12:16:16 +02:00
8f02bd1509 fix: avoid addition by undefined when adding skill points (#2108)
Reviewed-on: OpenWF/SpaceNinjaServer#2108
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-01 03:12:14 -07:00
32f4c5105a feat: add update and start script for linux (#2107)
Reviewed-on: OpenWF/SpaceNinjaServer#2107
Co-authored-by: Sainan <sainan@calamity.gg>
Co-committed-by: Sainan <sainan@calamity.gg>
2025-06-01 03:12:07 -07:00
28da982c80 feat: renamePet (#2106)
Reviewed-on: OpenWF/SpaceNinjaServer#2106
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-30 11:42:45 -07:00
ae1850d6cd chore: update PE+ (#2105)
Reviewed-on: OpenWF/SpaceNinjaServer#2105
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-27 03:58:12 -07:00
b90bdd2783 chore: update french translation (#2104)
Reviewed-on: OpenWF/SpaceNinjaServer#2104
Co-authored-by: Vitruvio <vitruvio@noreply.localhost>
Co-committed-by: Vitruvio <vitruvio@noreply.localhost>
2025-05-26 01:46:19 -07:00
90f2b90398 chore: switch to official tsgo preview package (#2103)
Reviewed-on: OpenWF/SpaceNinjaServer#2103
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-24 01:48:50 -07:00
84916bf64e fix: resolve random relic booster pack for login reward (#2101)
Reviewed-on: OpenWF/SpaceNinjaServer#2101
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-24 01:48:41 -07:00
d41e4f7f56 chore: restart web server when ports in config have changed (#2100)
Reviewed-on: OpenWF/SpaceNinjaServer#2100
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-24 01:48:25 -07:00
082ae536f7 fix: more robust address detection (#2099)
Previously this had a few issues when using non-standard ports for HTTP(S)

Reviewed-on: OpenWF/SpaceNinjaServer#2099
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-24 01:48:10 -07:00
ba6cd47432 feat: initial support for multiple nightwave seasons (#2096)
Reviewed-on: OpenWF/SpaceNinjaServer#2096
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-23 06:12:54 -07:00
92d34fd69e chore: update shard removal costs for 38.6.0 (#2094)
Reviewed-on: OpenWF/SpaceNinjaServer#2094
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-21 22:16:44 -07:00
09b9683fa1 chore(webui): update to Spanish translation (#2095)
Reviewed-on: OpenWF/SpaceNinjaServer#2095
Co-authored-by: hxedcl <hxedcl@noreply.localhost>
Co-committed-by: hxedcl <hxedcl@noreply.localhost>
2025-05-21 22:16:00 -07:00
a47eccdec8 fix: provide response for FocusOperation.ActivateWay (#2092)
Closes #2091

Reviewed-on: OpenWF/SpaceNinjaServer#2092
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-21 04:51:36 -07:00
ffd410537e feat: syndicateMissionsRepeatable cheat (#2090)
Closes #2050

Reviewed-on: OpenWF/SpaceNinjaServer#2090
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-05-20 02:48:45 -07:00
79eab71aaf chore(webui): update to Spanish translation (#2089)
Reviewed-on: OpenWF/SpaceNinjaServer#2089
Co-authored-by: hxedcl <hxedcl@noreply.localhost>
Co-committed-by: hxedcl <hxedcl@noreply.localhost>
2025-05-18 19:06:35 -07:00
21164554a3 chore: some fixes to enter guild dojo on U15 (#2088)
Reviewed-on: OpenWF/SpaceNinjaServer#2088
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-18 19:06:24 -07:00
45c0da6ed8 chore: fix some questionable calls to model.findById 2025-05-18 12:55:51 +02:00
727f6837ba feat: instantFinishRivenChallenge cheat (#2087)
Closes #1952

Reviewed-on: OpenWF/SpaceNinjaServer#2087
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-05-18 01:33:16 -07:00
77a3b64f49 feat: support all nemesis manifests down to 26.0.0 (#2086)
Also fixes the ephemera chance for kuva lich manifest v2 and up

Reviewed-on: OpenWF/SpaceNinjaServer#2086
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-17 23:29:23 -07:00
ce59086f7d chore: handle vendor per-item count limits (#2084)
Reviewed-on: OpenWF/SpaceNinjaServer#2084
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-17 23:29:16 -07:00
9b0989f1df chore: add self-test for serverside vendors (#2083)
Reviewed-on: OpenWF/SpaceNinjaServer#2083
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-17 23:29:09 -07:00
b01376f703 chore: replace instances of find(x => x = ...) with indexOf(...) 2025-05-17 09:17:03 +02:00
870ff2dd2c feat: adjust server-side vendor prices according to syndicate standings (#2076)
For buying crew members from ticker

Reviewed-on: OpenWF/SpaceNinjaServer#2076
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-16 20:01:20 -07:00
1ac71a9b28 chore: auto-detect cycle duration for auto-generated vendors (#2077)
This also fixes the "time left to trade" showing incorrectly for fishmonger "daily special" vendors

Reviewed-on: OpenWF/SpaceNinjaServer#2077
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-16 20:00:50 -07:00
a622787500 fix: ensure guild advertisments vendor always has its 5 offers (#2078)
Because the per-bin limits are not respected right now, it was possible that some clan tiers simply don't have an offer some weeks.

Reviewed-on: OpenWF/SpaceNinjaServer#2078
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-16 20:00:44 -07:00
52c8802d57 feat: classic lich vanquish inbox mesage (#2074)
Closes #1897

Reviewed-on: OpenWF/SpaceNinjaServer#2074
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-14 21:14:04 -07:00
daf721f7cd chore: don't set a default avatar image type in inventory (#2075)
This seems to be somewhat of an issue for older versions, plus it's not really accurate anyway.

Reviewed-on: OpenWF/SpaceNinjaServer#2075
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-14 21:13:49 -07:00
f724073d93 feat: classic lich regalia rewards (#2073)
Closes #2068

Reviewed-on: OpenWF/SpaceNinjaServer#2073
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-13 22:28:59 -07:00
8fb676c906 feat: classic lich ephemera reward (#2067)
Reviewed-on: OpenWF/SpaceNinjaServer#2067
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-13 20:41:49 -07:00
2ce5cc4562 fix: handle converted lich as crew member (#2071)
saveLoadout was missing bigint support to properly store NemesisFingerprint, and crewMembers was missing handling for liches being set on-call (the only option available for them)

Reviewed-on: OpenWF/SpaceNinjaServer#2071
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-13 20:39:03 -07:00
099f12a197 feat: bounty chemistry bonus (#2070)
Re #388

Reviewed-on: OpenWF/SpaceNinjaServer#2070
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-13 20:38:52 -07:00
bfe2e93c76 feat: resource reward along with duviri decree (#2066)
Closes #561

Reviewed-on: OpenWF/SpaceNinjaServer#2066
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-13 20:38:37 -07:00
8b97bb4b0a feat: classic lich hints (#2064)
Closes #1923

Reviewed-on: OpenWF/SpaceNinjaServer#2064
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-13 20:37:30 -07:00
85a45a04ea fix: ensure that only one CrewMember is ever on call (#2069)
Reviewed-on: OpenWF/SpaceNinjaServer#2069
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-13 08:25:09 -07:00
2a40449604 chore: add TNemesisFaction 2025-05-13 12:21:20 +02:00
382f8c55ce chore: update Docker stuff (#2065)
Reviewed-on: OpenWF/SpaceNinjaServer#2065
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-05-13 01:47:09 -07:00
77513190e4 fix: incorrect droptable name for zariman tier c (#2062)
Fixes #2061

Reviewed-on: OpenWF/SpaceNinjaServer#2062
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-12 19:49:30 -07:00
5e8ce934c9 chore: clarify which category has a negative count 2025-05-12 06:59:20 +02:00
6de81c2b41 chore: handle LasrianTankSteelPathDropTable for DROP_MOD (#2057)
Closes #2056

Reviewed-on: OpenWF/SpaceNinjaServer#2057
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-11 21:53:16 -07:00
4c5ac4f03a chore(webui): update German translation (#2059)
Reviewed-on: OpenWF/SpaceNinjaServer#2059
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-05-11 02:16:23 -07:00
d6f4c1a035 chore(webui): update to Spanish translation (#2058)
Reviewed-on: OpenWF/SpaceNinjaServer#2058
Co-authored-by: hxedcl <hxedcl@noreply.localhost>
Co-committed-by: hxedcl <hxedcl@noreply.localhost>
2025-05-11 00:02:57 -07:00
c58c70c4ce chore: update json-with-bigint minimum required version 2025-05-11 08:29:13 +02:00
3e1e19d6c5 feat: dontSubtractVoidTraces cheat (#2055)
Closes #2051

Reviewed-on: OpenWF/SpaceNinjaServer#2055
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-10 19:12:42 -07:00
2521733e55 fix: exclude open worlds from archon hunt (#2054)
Closes #2048

Reviewed-on: OpenWF/SpaceNinjaServer#2054
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-10 19:12:34 -07:00
d5297d3547 fix(webui): sidebar toggler not showing up on small screens (#2053)
Closes #2049

Reviewed-on: OpenWF/SpaceNinjaServer#2053
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-10 19:12:25 -07:00
5bc39aac8a fix: login failure on U22.8 (#2044)
2018.01.04.13.12

Reviewed-on: OpenWF/SpaceNinjaServer#2044
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-10 19:12:17 -07:00
b201508fa1 chore(webui): update German translation (#2046)
Reviewed-on: OpenWF/SpaceNinjaServer#2046
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-05-10 03:44:55 -07:00
5f9ae2aef6 chore(webui): update to Spanish translation (#2045)
Reviewed-on: OpenWF/SpaceNinjaServer#2045
Co-authored-by: hxedcl <hxedcl@noreply.localhost>
Co-committed-by: hxedcl <hxedcl@noreply.localhost>
2025-05-10 03:44:42 -07:00
b451c73598 chore: handle mods picked up in mission on U19 (#2042)
Reviewed-on: OpenWF/SpaceNinjaServer#2042
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-09 21:38:42 -07:00
9d4bce852e feat: the circuit (#2039)
Closes #1965

Closes #2041

Reviewed-on: OpenWF/SpaceNinjaServer#2039
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-09 21:38:13 -07:00
c83e732b88 feat: gifting bonus (#2036)
Closes #2014

Reviewed-on: OpenWF/SpaceNinjaServer#2036
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-09 21:37:59 -07:00
3d13ec311e feat: claimingBlueprintRefundsIngredients cheat (#2034)
Closes #1922

Reviewed-on: OpenWF/SpaceNinjaServer#2034
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-09 21:37:28 -07:00
31043b55de feat: batch remove friends (#2032)
Closes #1947

Reviewed-on: OpenWF/SpaceNinjaServer#2032
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-09 21:37:09 -07:00
ab32728c47 fix: don't give assassination blueprint reward for archon hunt (#2031)
Closes #2025

Reviewed-on: OpenWF/SpaceNinjaServer#2031
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-09 21:36:49 -07:00
3fc2dccf81 chore: use 64-bit RNG everywhere (#2030)
Closes #2026

Reviewed-on: OpenWF/SpaceNinjaServer#2030
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-09 21:36:22 -07:00
1084932afb fix: only set IsNew flag if the ItemType is new (#2028)
Reviewed-on: OpenWF/SpaceNinjaServer#2028
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-09 21:35:58 -07:00
f9b3fecc10 chore: some initial handling of legacy oid format (#2033)
This at least allows mission inventory update to succeed on U19.5 and below.

Reviewed-on: OpenWF/SpaceNinjaServer#2033
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-09 00:20:54 -07:00
7a51fab5d3 chore: address some questionable calls to DocumentArray.id 2025-05-09 07:18:25 +02:00
0e255067a8 chore: increase seed ranges to be more accurate (#2029)
Reviewed-on: OpenWF/SpaceNinjaServer#2029
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-08 18:00:22 -07:00
8d788d38a5 chore: remove query for ship in getShipController (#2022)
as far as I can tell, the ShipAttachments and SkinFlavourItem are just here due to the fact that the type from ShipExterior is being reused, but they aren't actually needed because the interior can't have attachments or flavour items - and if it could, they would be different from the exterior anyway.

Reviewed-on: OpenWF/SpaceNinjaServer#2022
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-08 17:37:00 -07:00
bdd5ade2eb feat: kuva siphon mission rewards (#2023)
Closes #1955

Reviewed-on: OpenWF/SpaceNinjaServer#2023
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-08 08:22:27 -07:00
3ff7e4264c chore: npm update (#2021)
Reviewed-on: OpenWF/SpaceNinjaServer#2021
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-08 08:22:06 -07:00
29b54b52dd feat: place conclave console decoration by default for new accounts (#2019)
Closes #1908

Reviewed-on: OpenWF/SpaceNinjaServer#2019
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-08 08:21:36 -07:00
dab8c6c8ba feat: static rewards for completion of arbitration mission (#2017)
Closes #1954

Reviewed-on: OpenWF/SpaceNinjaServer#2017
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-08 08:21:19 -07:00
bdb4c8fd7c fear: add InGameMarket to worldState (#2015)
To roughly match the market as seen on live, but excluding the "premium bundles" section.

Closes #1906

Reviewed-on: OpenWF/SpaceNinjaServer#2015
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-08 08:20:53 -07:00
3bcac1459b feat: track LastLogin to provide it for friends & clan members (#2013)
Reviewed-on: OpenWF/SpaceNinjaServer#2013
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-08 08:20:45 -07:00
b987e01811 fix: ships having wrong format in inventory response (#2018)
Reviewed-on: OpenWF/SpaceNinjaServer#2018
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-08 04:51:03 -07:00
d831732513 chore: update PE+ (#2020)
Reviewed-on: OpenWF/SpaceNinjaServer#2020
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-08 04:32:32 -07:00
42c63ecbe8 chore(webui): use name for zanuka from datalist instead of webui loc (#2012)
Reviewed-on: OpenWF/SpaceNinjaServer#2012
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-05-07 20:15:19 -07:00
0319031e13 chore(webui): Update Russian translation (#2010)
Reviewed-on: OpenWF/SpaceNinjaServer#2010
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-05-07 20:15:09 -07:00
1f3bb88910 feat: bounty bonus standing (#2009)
Closes #2007

Reviewed-on: OpenWF/SpaceNinjaServer#2009
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-05-07 20:15:00 -07:00
6171b36479 fix: use correct day when providing pre-rollover syndicate missions (#2006)
Closes #2005

Reviewed-on: OpenWF/SpaceNinjaServer#2006
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-07 20:14:34 -07:00
c56507e12d feat: friends (#2004)
Closes #1288

Reviewed-on: OpenWF/SpaceNinjaServer#2004
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-07 20:14:21 -07:00
e545ecf767 fix: restrict sortie mission types based on what the tileset supports (#2003)
Closes #2000

Reviewed-on: OpenWF/SpaceNinjaServer#2003
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-07 20:14:05 -07:00
58549c1488 fix: exclude railjack missions from archon hunt (#2002)
Closes #2001

Reviewed-on: OpenWF/SpaceNinjaServer#2002
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-07 20:13:45 -07:00
bb606f3a95 fix: get bounty info by id to handle rollover (#1998)
Closes #1988

Reviewed-on: OpenWF/SpaceNinjaServer#1998
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-06 19:05:23 -07:00
4b12fe12cb feat: handle mechsuits in sellController (#1996)
Closes #1995

Reviewed-on: OpenWF/SpaceNinjaServer#1996
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-06 19:04:53 -07:00
4f28688837 feat: add CompletedVorsPrize to getAccountInfo response (#1994)
Closes #1941

Reviewed-on: OpenWF/SpaceNinjaServer#1994
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-06 19:04:39 -07:00
203b3e20d9 fix: refuse to give starting gear again (#1993)
Reviewed-on: OpenWF/SpaceNinjaServer#1993
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-06 19:04:22 -07:00
005690daa4 fix: handle "skip prologue" to visit orbiter in U14 (#1999)
Reviewed-on: OpenWF/SpaceNinjaServer#1999
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-06 07:37:30 -07:00
5fefd189af fix: login failure on U15 (#1997)
Reviewed-on: OpenWF/SpaceNinjaServer#1997
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-06 06:05:12 -07:00
f6cbc02c47 fix: ignore client providing not-an-id in recipe ids array (#1990)
Closes #1989

Reviewed-on: OpenWF/SpaceNinjaServer#1990
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-06 02:29:23 -07:00
da6d75c748 fix: don't give sortie assassination rewards if mission type differs (#1992)
Closes #1987

Reviewed-on: OpenWF/SpaceNinjaServer#1992
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-06 02:29:11 -07:00
460deed3ed fix: login failure on U16 (#1991)
Reviewed-on: OpenWF/SpaceNinjaServer#1991
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-06 02:29:03 -07:00
4e57bcd1ae fix: login failure on U17 (#1986)
Reviewed-on: OpenWF/SpaceNinjaServer#1986
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-05 18:09:03 -07:00
e2fe406017 fix: always use rotation A for Profit-Taker bounty rewards (#1981)
Closes #1964

Reviewed-on: OpenWF/SpaceNinjaServer#1981
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-05 18:03:53 -07:00
c53dc2fd02 fix: remove non-existent sortie modifiers (#1980)
Closes #1976

Reviewed-on: OpenWF/SpaceNinjaServer#1980
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-05 18:03:44 -07:00
cfa3586f64 fix: don't provide syndicate missions in advance (#1979)
Closes #1975

Reviewed-on: OpenWF/SpaceNinjaServer#1979
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-05 18:03:36 -07:00
238af294fe fix: login failure on U18 (#1983)
Reviewed-on: OpenWF/SpaceNinjaServer#1983
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-05 04:57:44 -07:00
1914fd8f10 fix: venus bounties having wrong reward tables (#1978)
Due to missing / on six of the reward tables.

Fixes #1977.

Reviewed-on: OpenWF/SpaceNinjaServer#1978
Co-authored-by: VampireKitten <dynamightkobold@gmail.com>
Co-committed-by: VampireKitten <dynamightkobold@gmail.com>
2025-05-04 17:31:36 -07:00
ec4af075b5 fix: login failure on U21 (#1974)
Reviewed-on: OpenWF/SpaceNinjaServer#1974
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-04 17:30:48 -07:00
118 changed files with 4847 additions and 3185 deletions

View File

@ -10,6 +10,8 @@ jobs:
uses: actions/checkout@v4.1.2
- name: Setup Node.js environment
uses: actions/setup-node@v4.0.2
with:
node-version: ">=20.6.0"
- run: npm ci
- run: cp config.json.example config.json
- run: npm run verify

View File

@ -14,6 +14,8 @@ ENV APP_INFINITE_PLATINUM=false
ENV APP_INFINITE_ENDO=false
ENV APP_INFINITE_REGAL_AYA=false
ENV APP_INFINITE_HELMINTH_MATERIALS=false
ENV APP_CLAIMING_BLUEPRINT_REFUNDS_INGREDIENTS=false
ENV APP_DONT_SUBTRACT_VOIDTRACES=false
ENV APP_DONT_SUBTRACT_CONSUMABLES=false
ENV APP_UNLOCK_ALL_SHIP_FEATURES=false
ENV APP_UNLOCK_ALL_SHIP_DECORATIONS=false
@ -30,6 +32,8 @@ ENV APP_NO_MASTERY_RANK_UP_COOLDOWN=false
ENV APP_NO_VENDOR_PURCHASE_LIMITS=true
ENV APP_NO_DEATH_MARKS=false
ENV APP_NO_KIM_COOLDOWNS=false
ENV APP_SYNDICATE_MISSIONS_REPEATABLE=false
ENV APP_INSTANT_FINISH_RIVEN_CHALLENGE=false
ENV APP_INSTANT_RESOURCE_EXTRACTOR_DRONES=false
ENV APP_NO_RESOURCE_EXTRACTOR_DRONES_DAMAGE=false
ENV APP_SKIP_CLAN_KEY_CRAFTING=false

View File

@ -14,4 +14,22 @@ SpaceNinjaServer requires a `config.json`. To set it up, you can copy the [confi
- `logger.level` can be `fatal`, `error`, `warn`, `info`, `http`, `debug`, or `trace`.
- `myIrcAddresses` can be used to point to an IRC server. If not provided, defaults to `[ myAddress ]`.
- `worldState.lockTime` will lock the time provided in worldState if nonzero, e.g. `1743202800` for night in POE.
- `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.
- `worldState.vallisOverride` can be set to `warm` or `cold` to lock the temperature on Orb Vallis.
- `worldState.nightwaveOverride` will lock the nightwave season, assuming the client is new enough for it. Valid values:
- `RadioLegionIntermission13Syndicate` for Nora's Mix Vol. 9
- `RadioLegionIntermission12Syndicate` for Nora's Mix Vol. 8
- `RadioLegionIntermission11Syndicate` for Nora's Mix Vol. 7
- `RadioLegionIntermission10Syndicate` for Nora's Mix Vol. 6
- `RadioLegionIntermission9Syndicate` for Nora's Mix Vol. 5
- `RadioLegionIntermission8Syndicate` for Nora's Mix Vol. 4
- `RadioLegionIntermission7Syndicate` for Nora's Mix Vol. 3
- `RadioLegionIntermission6Syndicate` for Nora's Mix Vol. 2
- `RadioLegionIntermission5Syndicate` for Nora's Mix Vol. 1
- `RadioLegionIntermission4Syndicate` for Nora's Choice
- `RadioLegionIntermission3Syndicate` for Intermission III
- `RadioLegion3Syndicate` for Glassmaker
- `RadioLegionIntermission2Syndicate` for Intermission II
- `RadioLegion2Syndicate` for The Emissary
- `RadioLegionIntermissionSyndicate` for Intermission I
- `RadioLegionSyndicate` for The Wolf of Saturn Six

View File

@ -3,7 +3,7 @@
echo Updating SpaceNinjaServer...
git fetch --prune
git stash
git reset --hard origin/main
git checkout -f origin/main
if exist static\data\0\ (
echo Updating stripped assets...

23
UPDATE AND START SERVER.sh Executable file
View File

@ -0,0 +1,23 @@
#!/bin/bash
echo "Updating SpaceNinjaServer..."
git fetch --prune
git stash
git checkout -f origin/main
if [ -d "static/data/0/" ]; then
echo "Updating stripped assets..."
cd static/data/0/
git pull
cd ../../../
fi
echo "Updating dependencies..."
npm i --omit=dev
npm run build
if [ $? -eq 0 ]; then
npm run start
echo "SpaceNinjaServer seems to have crashed."
fi

View File

@ -19,6 +19,8 @@
"infiniteEndo": false,
"infiniteRegalAya": false,
"infiniteHelminthMaterials": false,
"claimingBlueprintRefundsIngredients": false,
"dontSubtractVoidTraces": false,
"dontSubtractConsumables": false,
"unlockAllShipFeatures": false,
"unlockAllShipDecorations": false,
@ -36,6 +38,8 @@
"noVendorPurchaseLimits": true,
"noDeathMarks": false,
"noKimCooldowns": false,
"syndicateMissionsRepeatable": false,
"instantFinishRivenChallenge": false,
"instantResourceExtractorDrones": false,
"noResourceExtractorDronesDamage": false,
"skipClanKeyCrafting": false,
@ -46,11 +50,17 @@
"noDojoResearchTime": false,
"fastClanAscension": false,
"spoofMasteryRank": -1,
"nightwaveStandingMultiplier": 1,
"worldState": {
"creditBoost": false,
"affinityBoost": false,
"resourceBoost": false,
"starDays": true,
"lockTime": 0
"eidolonOverride": "",
"vallisOverride": "",
"nightwaveOverride": ""
},
"dev": {
"keepVendorsExpired": false
}
}

View File

@ -21,6 +21,8 @@ services:
# APP_INFINITE_ENDO: false
# APP_INFINITE_REGAL_AYA: false
# APP_INFINITE_HELMINTH_MATERIALS: false
# APP_CLAIMING_BLUEPRINT_REFUNDS_INGREDIENTS: false
# APP_DONT_SUBTRACT_VOIDTRACES: false
# APP_DONT_SUBTRACT_CONSUMABLES: false
# APP_UNLOCK_ALL_SHIP_FEATURES: false
# APP_UNLOCK_ALL_SHIP_DECORATIONS: false
@ -37,6 +39,8 @@ services:
# APP_NO_VENDOR_PURCHASE_LIMITS: true
# APP_NO_DEATH_MARKS: false
# APP_NO_KIM_COOLDOWNS: false
# APP_SYNDICATE_MISSIONS_REPEATABLE: false
# APP_INSTANT_FINISH_RIVEN_CHALLENGE: false
# APP_INSTANT_RESOURCE_EXTRACTOR_DRONES: false
# APP_NO_RESOURCE_EXTRACTOR_DRONES_DAMAGE: false
# APP_SKIP_CLAN_KEY_CRAFTING: false

665
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -20,28 +20,24 @@
"@types/morgan": "^1.9.9",
"crc-32": "^1.2.2",
"express": "^5",
"json-with-bigint": "^3.2.2",
"json-with-bigint": "^3.4.4",
"mongoose": "^8.11.0",
"morgan": "^1.10.0",
"ncp": "^2.0.0",
"typescript": "^5.5",
"warframe-public-export-plus": "^0.5.59",
"warframe-public-export-plus": "^0.5.66",
"warframe-riven-info": "^0.1.2",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0"
},
"devDependencies": {
"@rxliuli/tsgo": "^2025.3.31",
"@typescript-eslint/eslint-plugin": "^8.28.0",
"@typescript-eslint/parser": "^8.28.0",
"@typescript/native-preview": "^7.0.0-dev.20250523.1",
"eslint": "^8",
"eslint-plugin-prettier": "^5.2.5",
"prettier": "^3.5.3",
"ts-node-dev": "^2.0.0",
"tsconfig-paths": "^4.2.0"
},
"engines": {
"node": ">=18.15.0",
"npm": ">=9.5.0"
}
}

View File

@ -21,6 +21,13 @@ app.use((req, _res, next) => {
if (req.headers["content-encoding"] == "ezip" || req.headers["content-encoding"] == "e") {
req.headers["content-encoding"] = undefined;
}
// U18 uses application/x-www-form-urlencoded even tho the data is JSON which Express doesn't like.
// U17 sets no Content-Type at all, which Express also doesn't like.
if (!req.headers["content-type"] || req.headers["content-type"] == "application/x-www-form-urlencoded") {
req.headers["content-type"] = "application/octet-stream";
}
next();
});

View File

@ -1,11 +1,16 @@
import { toOid } from "@/src/helpers/inventoryHelpers";
import { createVeiledRivenFingerprint, rivenRawToRealWeighted } from "@/src/helpers/rivenHelper";
import {
createVeiledRivenFingerprint,
createUnveiledRivenFingerprint,
rivenRawToRealWeighted
} from "@/src/helpers/rivenHelper";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { addMods, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getRandomElement } from "@/src/services/rngService";
import { RequestHandler } from "express";
import { ExportUpgrades } from "warframe-public-export-plus";
import { config } from "@/src/services/configService";
export const activateRandomModController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
@ -18,7 +23,9 @@ export const activateRandomModController: RequestHandler = async (req, res) => {
}
]);
const rivenType = getRandomElement(rivenRawToRealWeighted[request.ItemType])!;
const fingerprint = createVeiledRivenFingerprint(ExportUpgrades[rivenType]);
const fingerprint = config.instantFinishRivenChallenge
? createUnveiledRivenFingerprint(ExportUpgrades[rivenType])
: createVeiledRivenFingerprint(ExportUpgrades[rivenType]);
const upgradeIndex =
inventory.Upgrades.push({
ItemType: rivenType,

View File

@ -0,0 +1,60 @@
import { toOid } from "@/src/helpers/inventoryHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Friendship } from "@/src/models/friendModel";
import { addAccountDataToFriendInfo, addInventoryDataToFriendInfo } from "@/src/services/friendService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IFriendInfo } from "@/src/types/friendTypes";
import { RequestHandler } from "express";
export const addFriendController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const payload = getJSONfromString<IAddFriendRequest>(String(req.body));
const promises: Promise<void>[] = [];
const newFriends: IFriendInfo[] = [];
if (payload.friend == "all") {
const [internalFriendships, externalFriendships] = await Promise.all([
Friendship.find({ owner: accountId }, "friend"),
Friendship.find({ friend: accountId }, "owner")
]);
for (const externalFriendship of externalFriendships) {
if (!internalFriendships.find(x => x.friend.equals(externalFriendship.owner))) {
promises.push(
Friendship.insertOne({
owner: accountId,
friend: externalFriendship.owner,
Note: externalFriendship.Note // TOVERIFY: Should the note be copied when accepting a friend request?
}) as unknown as Promise<void>
);
newFriends.push({
_id: toOid(externalFriendship.owner)
});
}
}
} else {
const externalFriendship = await Friendship.findOne({ owner: payload.friend, friend: accountId }, "Note");
if (externalFriendship) {
promises.push(
Friendship.insertOne({
owner: accountId,
friend: payload.friend,
Note: externalFriendship.Note
}) as unknown as Promise<void>
);
newFriends.push({
_id: { $oid: payload.friend }
});
}
}
for (const newFriend of newFriends) {
promises.push(addAccountDataToFriendInfo(newFriend));
promises.push(addInventoryDataToFriendInfo(newFriend));
}
await Promise.all(promises);
res.json({
Friends: newFriends
});
};
interface IAddFriendRequest {
friend: string; // oid or "all" in which case all=1 is also a query parameter
}

View File

@ -2,7 +2,7 @@ import { toOid } from "@/src/helpers/inventoryHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Account, Ignore } from "@/src/models/loginModel";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IFriendInfo } from "@/src/types/guildTypes";
import { IFriendInfo } from "@/src/types/friendTypes";
import { RequestHandler } from "express";
export const addIgnoredUserController: RequestHandler = async (req, res) => {

View File

@ -0,0 +1,52 @@
import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Friendship } from "@/src/models/friendModel";
import { Account } from "@/src/models/loginModel";
import { addInventoryDataToFriendInfo, areFriendsOfFriends } from "@/src/services/friendService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IFriendInfo } from "@/src/types/friendTypes";
import { RequestHandler } from "express";
export const addPendingFriendController: RequestHandler = async (req, res) => {
const payload = getJSONfromString<IAddPendingFriendRequest>(String(req.body));
const account = await Account.findOne({ DisplayName: payload.friend });
if (!account) {
res.status(400).end();
return;
}
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(account._id.toString(), "Settings");
if (
inventory.Settings?.FriendInvRestriction == "GIFT_MODE_NONE" ||
(inventory.Settings?.FriendInvRestriction == "GIFT_MODE_FRIENDS" &&
!(await areFriendsOfFriends(account._id, accountId)))
) {
res.status(400).send("Friend Invite Restriction");
return;
}
await Friendship.insertOne({
owner: accountId,
friend: account._id,
Note: payload.message
});
const friendInfo: IFriendInfo = {
_id: toOid(account._id),
DisplayName: account.DisplayName,
LastLogin: toMongoDate(account.LastLogin),
Note: payload.message
};
await addInventoryDataToFriendInfo(friendInfo);
res.json({
Friend: friendInfo
});
};
interface IAddPendingFriendRequest {
friend: string;
message: string;
}

View File

@ -1,7 +1,7 @@
import { getJSONfromString, regexEscape } from "@/src/helpers/stringHelpers";
import { Alliance, AllianceMember, Guild, GuildMember } from "@/src/models/guildModel";
import { createMessage } from "@/src/services/inboxService";
import { getInventory } from "@/src/services/inventoryService";
import { getEffectiveAvatarImageType, getInventory } from "@/src/services/inventoryService";
import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
import { logger } from "@/src/utils/logger";
@ -75,7 +75,7 @@ export const addToAllianceController: RequestHandler = async (req, res) => {
const invitedClanOwnerMember = (await GuildMember.findOne({ guildId: guilds[0]._id, rank: 0 }))!;
const senderInventory = await getInventory(account._id.toString(), "ActiveAvatarImageType");
const senderGuild = (await Guild.findById(allianceMember.guildId, "Name"))!;
const alliance = (await Alliance.findById(req.query.allianceId, "Name"))!;
const alliance = (await Alliance.findById(req.query.allianceId as string, "Name"))!;
await createMessage(invitedClanOwnerMember.accountId, [
{
sndr: getSuffixedName(account),
@ -95,7 +95,7 @@ export const addToAllianceController: RequestHandler = async (req, res) => {
}
],
sub: "/Lotus/Language/Menu/Mailbox_AllianceInvite_Title",
icon: ExportFlavour[senderInventory.ActiveAvatarImageType].icon,
icon: ExportFlavour[getEffectiveAvatarImageType(senderInventory)].icon,
contextInfo: alliance._id.toString(),
highPriority: true,
acceptAction: "ALLIANCE_INVITE",

View File

@ -1,8 +1,10 @@
import { toMongoDate } from "@/src/helpers/inventoryHelpers";
import { Guild, GuildMember } from "@/src/models/guildModel";
import { Account } from "@/src/models/loginModel";
import { fillInInventoryDataForGuildMember, hasGuildPermission } from "@/src/services/guildService";
import { addInventoryDataToFriendInfo, areFriends } from "@/src/services/friendService";
import { hasGuildPermission } from "@/src/services/guildService";
import { createMessage } from "@/src/services/inboxService";
import { getInventory } from "@/src/services/inventoryService";
import { getEffectiveAvatarImageType, getInventory } from "@/src/services/inventoryService";
import { getAccountForRequest, getAccountIdForRequest, getSuffixedName } from "@/src/services/loginService";
import { IOid } from "@/src/types/commonTypes";
import { GuildPermission, IGuildMemberClient } from "@/src/types/guildTypes";
@ -22,15 +24,18 @@ export const addToGuildController: RequestHandler = async (req, res) => {
return;
}
const senderAccount = await getAccountForRequest(req);
const inventory = await getInventory(account._id.toString(), "Settings");
// TODO: Also consider GIFT_MODE_FRIENDS once friends are implemented
if (inventory.Settings?.GuildInvRestriction == "GIFT_MODE_NONE") {
if (
inventory.Settings?.GuildInvRestriction == "GIFT_MODE_NONE" ||
(inventory.Settings?.GuildInvRestriction == "GIFT_MODE_FRIENDS" &&
!(await areFriends(account._id, senderAccount._id)))
) {
res.status(400).json("Invite restricted");
return;
}
const guild = (await Guild.findById(payload.GuildId.$oid, "Name Ranks"))!;
const senderAccount = await getAccountForRequest(req);
if (!(await hasGuildPermission(guild, senderAccount._id.toString(), GuildPermission.Recruiter))) {
res.status(400).json("Invalid permission");
}
@ -59,7 +64,7 @@ export const addToGuildController: RequestHandler = async (req, res) => {
}
],
sub: "/Lotus/Language/Menu/Mailbox_ClanInvite_Title",
icon: ExportFlavour[senderInventory.ActiveAvatarImageType].icon,
icon: ExportFlavour[getEffectiveAvatarImageType(senderInventory)].icon,
contextInfo: payload.GuildId.$oid,
highPriority: true,
acceptAction: "GUILD_INVITE",
@ -71,10 +76,11 @@ export const addToGuildController: RequestHandler = async (req, res) => {
const member: IGuildMemberClient = {
_id: { $oid: account._id.toString() },
DisplayName: account.DisplayName,
LastLogin: toMongoDate(account.LastLogin),
Rank: 7,
Status: 2
};
await fillInInventoryDataForGuildMember(member);
await addInventoryDataToFriendInfo(member);
res.json({ NewMember: member });
} else if ("RequestMsg" in payload) {
// Player applying to join a clan

View File

@ -0,0 +1,27 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const adoptPetController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "KubrowPets");
const data = getJSONfromString<IAdoptPetRequest>(String(req.body));
const details = inventory.KubrowPets.id(data.petId)!.Details!;
details.Name = data.name;
await inventory.save();
res.json({
petId: data.petId,
newName: data.name
} satisfies IAdoptPetResponse);
};
interface IAdoptPetRequest {
petId: string;
name: string;
}
interface IAdoptPetResponse {
petId: string;
newName: string;
}

View File

@ -1,9 +1,9 @@
import { toOid } from "@/src/helpers/inventoryHelpers";
import { fromOid, toOid } from "@/src/helpers/inventoryHelpers";
import { createVeiledRivenFingerprint, rivenRawToRealWeighted } from "@/src/helpers/rivenHelper";
import { addMiscItems, addMods, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getRandomElement, getRandomWeightedReward, getRandomWeightedRewardUc } from "@/src/services/rngService";
import { IOid } from "@/src/types/commonTypes";
import { IUpgradeFromClient } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express";
import { ExportBoosterPacks, ExportUpgrades, TRarity } from "warframe-public-export-plus";
@ -24,7 +24,7 @@ export const artifactTransmutationController: RequestHandler = async (req, res)
]);
payload.Consumed.forEach(upgrade => {
inventory.Upgrades.pull({ _id: upgrade.ItemId.$oid });
inventory.Upgrades.pull({ _id: fromOid(upgrade.ItemId) });
});
const rawRivenType = getRandomRawRivenType();
@ -57,8 +57,8 @@ export const artifactTransmutationController: RequestHandler = async (req, res)
payload.Consumed.forEach(upgrade => {
const meta = ExportUpgrades[upgrade.ItemType];
counts[meta.rarity] += upgrade.ItemCount;
if (upgrade.ItemId.$oid != "000000000000000000000000") {
inventory.Upgrades.pull({ _id: upgrade.ItemId.$oid });
if (fromOid(upgrade.ItemId) != "000000000000000000000000") {
inventory.Upgrades.pull({ _id: fromOid(upgrade.ItemId) });
} else {
addMods(inventory, [
{
@ -128,24 +128,14 @@ const getRandomRawRivenType = (): string => {
};
interface IArtifactTransmutationRequest {
Upgrade: IAgnosticUpgradeClient;
Upgrade: IUpgradeFromClient;
LevelDiff: number;
Consumed: IAgnosticUpgradeClient[];
Consumed: IUpgradeFromClient[];
Cost: number;
FusionPointCost: number;
RivenTransmute?: boolean;
}
interface IAgnosticUpgradeClient {
ItemType: string;
ItemId: IOid;
FromSKU: boolean;
UpgradeFingerprint: string;
PendingRerollFingerprint: string;
ItemCount: number;
LastAdded: IOid;
}
const specialModSets: string[][] = [
[
"/Lotus/Upgrades/Mods/Immortal/ImmortalOneMod",

View File

@ -4,9 +4,9 @@
import { RequestHandler } from "express";
import { logger } from "@/src/utils/logger";
import { getRecipe } from "@/src/services/itemDataService";
import { IOid } from "@/src/types/commonTypes";
import { IOid, IOidWithLegacySupport } from "@/src/types/commonTypes";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getAccountForRequest } from "@/src/services/loginService";
import {
getInventory,
updateCurrency,
@ -17,8 +17,11 @@ import {
} from "@/src/services/inventoryService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
import { toOid } from "@/src/helpers/inventoryHelpers";
import { InventorySlot, IPendingRecipeDatabase, Status } from "@/src/types/inventoryTypes/inventoryTypes";
import { toOid2 } from "@/src/helpers/inventoryHelpers";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { IRecipe } from "warframe-public-export-plus";
import { config } from "@/src/services/configService";
interface IClaimCompletedRecipeRequest {
RecipeIds: IOid[];
@ -26,10 +29,8 @@ interface IClaimCompletedRecipeRequest {
export const claimCompletedRecipeController: RequestHandler = async (req, res) => {
const claimCompletedRecipeRequest = getJSONfromString<IClaimCompletedRecipeRequest>(String(req.body));
const accountId = await getAccountIdForRequest(req);
if (!accountId) throw new Error("no account id");
const inventory = await getInventory(accountId);
const account = await getAccountForRequest(req);
const inventory = await getInventory(account._id.toString());
const pendingRecipe = inventory.PendingRecipes.id(claimCompletedRecipeRequest.RecipeIds[0].$oid);
if (!pendingRecipe) {
throw new Error(`no pending recipe found with id ${claimCompletedRecipeRequest.RecipeIds[0].$oid}`);
@ -48,40 +49,14 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
}
if (req.query.cancel) {
const inventoryChanges: IInventoryChanges = {
...updateCurrency(inventory, recipe.buildPrice * -1, false)
};
const equipmentIngredients = new Set();
for (const category of ["LongGuns", "Pistols", "Melee"] as const) {
if (pendingRecipe[category]) {
pendingRecipe[category].forEach(item => {
const index = inventory[category].push(item) - 1;
inventoryChanges[category] ??= [];
inventoryChanges[category].push(inventory[category][index].toJSON<IEquipmentClient>());
equipmentIngredients.add(item.ItemType);
occupySlot(inventory, InventorySlot.WEAPONS, false);
inventoryChanges.WeaponBin ??= { Slots: 0 };
inventoryChanges.WeaponBin.Slots -= 1;
});
}
}
for (const ingredient of recipe.ingredients) {
if (!equipmentIngredients.has(ingredient.ItemType)) {
combineInventoryChanges(
inventoryChanges,
await addItem(inventory, ingredient.ItemType, ingredient.ItemCount)
);
}
}
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.
} else {
logger.debug("Claiming Recipe", { recipe, pendingRecipe });
let BrandedSuits: undefined | IOid[];
let BrandedSuits: undefined | IOidWithLegacySupport[];
if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") {
inventory.PendingSpectreLoadouts ??= [];
inventory.SpectreLoadouts ??= [];
@ -106,7 +81,7 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
inventory.BrandedSuits!.findIndex(x => x.equals(pendingRecipe.SuitToUnbrand)),
1
);
BrandedSuits = [toOid(pendingRecipe.SuitToUnbrand!)];
BrandedSuits = [toOid2(pendingRecipe.SuitToUnbrand!, account.BuildLabel)];
}
let InventoryChanges: IInventoryChanges = {};
@ -130,7 +105,21 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
...updateCurrency(inventory, cost, true)
};
}
if (recipe.secretIngredientAction != "SIA_UNBRAND") {
if (recipe.secretIngredientAction == "SIA_CREATE_KUBROW") {
const pet = inventory.KubrowPets.id(pendingRecipe.KubrowPet!)!;
if (pet.Details!.HatchDate!.getTime() > Date.now()) {
pet.Details!.HatchDate = new Date();
}
let canSetActive = true;
for (const pet of inventory.KubrowPets) {
if (pet.Details!.Status == Status.StatusAvailable) {
canSetActive = false;
break;
}
}
pet.Details!.Status = canSetActive ? Status.StatusAvailable : Status.StatusStasis;
} else if (recipe.secretIngredientAction != "SIA_UNBRAND") {
InventoryChanges = {
...InventoryChanges,
...(await addItem(
@ -143,7 +132,46 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
))
};
}
if (
config.claimingBlueprintRefundsIngredients &&
recipe.secretIngredientAction != "SIA_CREATE_KUBROW" // Can't refund the egg
) {
await refundRecipeIngredients(inventory, InventoryChanges, recipe, pendingRecipe);
}
await inventory.save();
res.json({ InventoryChanges, BrandedSuits });
}
};
const refundRecipeIngredients = async (
inventory: TInventoryDatabaseDocument,
inventoryChanges: IInventoryChanges,
recipe: IRecipe,
pendingRecipe: IPendingRecipeDatabase
): Promise<void> => {
updateCurrency(inventory, recipe.buildPrice * -1, false, inventoryChanges);
const equipmentIngredients = new Set();
for (const category of ["LongGuns", "Pistols", "Melee"] as const) {
if (pendingRecipe[category]) {
pendingRecipe[category].forEach(item => {
const index = inventory[category].push(item) - 1;
inventoryChanges[category] ??= [];
inventoryChanges[category].push(inventory[category][index].toJSON<IEquipmentClient>());
equipmentIngredients.add(item.ItemType);
occupySlot(inventory, InventorySlot.WEAPONS, false);
inventoryChanges.WeaponBin ??= { Slots: 0 };
inventoryChanges.WeaponBin.Slots -= 1;
});
}
}
for (const ingredient of recipe.ingredients) {
if (!equipmentIngredients.has(ingredient.ItemType)) {
combineInventoryChanges(
inventoryChanges,
await addItem(inventory, ingredient.ItemType, ingredient.ItemCount)
);
}
}
};

View File

@ -62,7 +62,7 @@ export const confirmGuildInvitationGetController: RequestHandler = async (req, r
await guild.save();
res.json({
...(await getGuildClient(guild, account._id.toString())),
...(await getGuildClient(guild, account)),
InventoryChanges: inventoryChanges
});
} else {

View File

@ -1,5 +1,5 @@
import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getAccountForRequest } from "@/src/services/loginService";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Guild, GuildMember } from "@/src/models/guildModel";
import { createUniqueClanName, getGuildClient, giveClanKey } from "@/src/services/guildService";
@ -7,11 +7,11 @@ import { getInventory } from "@/src/services/inventoryService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
export const createGuildController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const account = await getAccountForRequest(req);
const payload = getJSONfromString<ICreateGuildRequest>(String(req.body));
// Remove pending applications for this account
await GuildMember.deleteMany({ accountId, status: 1 });
await GuildMember.deleteMany({ accountId: account._id, status: 1 });
// Create guild on database
const guild = new Guild({
@ -21,20 +21,20 @@ export const createGuildController: RequestHandler = async (req, res) => {
// Create guild member on database
await GuildMember.insertOne({
accountId: accountId,
accountId: account._id,
guildId: guild._id,
status: 0,
rank: 0
});
const inventory = await getInventory(accountId, "GuildId LevelKeys Recipes");
const inventory = await getInventory(account._id.toString(), "GuildId LevelKeys Recipes");
inventory.GuildId = guild._id;
const inventoryChanges: IInventoryChanges = {};
giveClanKey(inventory, inventoryChanges);
await inventory.save();
res.json({
...(await getGuildClient(guild, accountId)),
...(await getGuildClient(guild, account)),
InventoryChanges: inventoryChanges
});
};

View File

@ -1,4 +1,5 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { ICrewMemberClient } from "@/src/types/inventoryTypes/inventoryTypes";
@ -7,15 +8,23 @@ import { Types } from "mongoose";
export const crewMembersController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "CrewMembers");
const inventory = await getInventory(accountId, "CrewMembers NemesisHistory");
const data = getJSONfromString<ICrewMembersRequest>(String(req.body));
const dbCrewMember = inventory.CrewMembers.id(data.crewMember.ItemId.$oid)!;
dbCrewMember.AssignedRole = data.crewMember.AssignedRole;
dbCrewMember.SkillEfficiency = data.crewMember.SkillEfficiency;
dbCrewMember.WeaponConfigIdx = data.crewMember.WeaponConfigIdx;
dbCrewMember.WeaponId = new Types.ObjectId(data.crewMember.WeaponId.$oid);
dbCrewMember.Configs = data.crewMember.Configs;
dbCrewMember.SecondInCommand = data.crewMember.SecondInCommand;
if (data.crewMember.SecondInCommand) {
clearOnCall(inventory);
}
if (data.crewMember.ItemId.$oid == "000000000000000000000000") {
const convertedNemesis = inventory.NemesisHistory!.find(x => x.fp == data.crewMember.NemesisFingerprint)!;
convertedNemesis.SecondInCommand = data.crewMember.SecondInCommand;
} else {
const dbCrewMember = inventory.CrewMembers.id(data.crewMember.ItemId.$oid)!;
dbCrewMember.AssignedRole = data.crewMember.AssignedRole;
dbCrewMember.SkillEfficiency = data.crewMember.SkillEfficiency;
dbCrewMember.WeaponConfigIdx = data.crewMember.WeaponConfigIdx;
dbCrewMember.WeaponId = new Types.ObjectId(data.crewMember.WeaponId.$oid);
dbCrewMember.Configs = data.crewMember.Configs;
dbCrewMember.SecondInCommand = data.crewMember.SecondInCommand;
}
await inventory.save();
res.json({
crewMemberId: data.crewMember.ItemId.$oid,
@ -26,3 +35,20 @@ export const crewMembersController: RequestHandler = async (req, res) => {
interface ICrewMembersRequest {
crewMember: ICrewMemberClient;
}
const clearOnCall = (inventory: TInventoryDatabaseDocument): void => {
for (const cm of inventory.CrewMembers) {
if (cm.SecondInCommand) {
cm.SecondInCommand = false;
return;
}
}
if (inventory.NemesisHistory) {
for (const cm of inventory.NemesisHistory) {
if (cm.SecondInCommand) {
cm.SecondInCommand = false;
return;
}
}
}
};

View File

@ -1,60 +1,529 @@
import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory } from "@/src/services/inventoryService";
import { combineInventoryChanges, getInventory } from "@/src/services/inventoryService";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { TEndlessXpCategory } from "@/src/types/inventoryTypes/inventoryTypes";
import { IEndlessXpReward, IInventoryClient, TEndlessXpCategory } from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger";
import { ExportRewards, ICountedStoreItem } from "warframe-public-export-plus";
import { getRandomElement } from "@/src/services/rngService";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
export const endlessXpController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const payload = getJSONfromString<IEndlessXpRequest>(String(req.body));
inventory.EndlessXP ??= [];
const entry = inventory.EndlessXP.find(x => x.Category == payload.Category);
if (entry) {
entry.Choices = payload.Choices;
} else {
inventory.EndlessXP.push({
Category: payload.Category,
Choices: payload.Choices
});
}
await inventory.save();
res.json({
NewProgress: {
Category: payload.Category,
Earn: 0,
Claim: 0,
BonusAvailable: {
$date: {
$numberLong: "9999999999999"
}
},
Expiry: {
$date: {
$numberLong: "9999999999999"
}
},
Choices: payload.Choices,
PendingRewards: [
{
RequiredTotalXp: 190,
Rewards: [
{
StoreItem: "/Lotus/StoreItems/Upgrades/Mods/Aura/PlayerHealthAuraMod",
ItemCount: 1
}
]
}
// ...
]
if (payload.Mode == "r") {
const inventory = await getInventory(accountId, "EndlessXP");
inventory.EndlessXP ??= [];
let entry = inventory.EndlessXP.find(x => x.Category == payload.Category);
if (!entry) {
entry = {
Category: payload.Category,
Earn: 0,
Claim: 0,
Choices: payload.Choices,
PendingRewards: []
};
inventory.EndlessXP.push(entry);
}
});
const weekStart = 1734307200_000 + Math.trunc((Date.now() - 1734307200_000) / 604800000) * 604800000;
const weekEnd = weekStart + 604800000;
entry.Earn = 0;
entry.Claim = 0;
entry.BonusAvailable = new Date(weekStart);
entry.Expiry = new Date(weekEnd);
entry.Choices = payload.Choices;
entry.PendingRewards =
payload.Category == "EXC_HARD"
? generateHardModeRewards(payload.Choices)
: generateNormalModeRewards(payload.Choices);
await inventory.save();
res.json({
NewProgress: inventory.toJSON<IInventoryClient>().EndlessXP!.find(x => x.Category == payload.Category)!
});
} else if (payload.Mode == "c") {
const inventory = await getInventory(accountId);
const entry = inventory.EndlessXP!.find(x => x.Category == payload.Category)!;
const inventoryChanges: IInventoryChanges = {};
for (const reward of entry.PendingRewards) {
if (entry.Claim < reward.RequiredTotalXp && reward.RequiredTotalXp <= entry.Earn) {
combineInventoryChanges(
inventoryChanges,
(
await handleStoreItemAcquisition(
reward.Rewards[0].StoreItem,
inventory,
reward.Rewards[0].ItemCount
)
).InventoryChanges
);
}
}
entry.Claim = entry.Earn;
await inventory.save();
res.json({
InventoryChanges: inventoryChanges,
ClaimedXp: entry.Claim
});
} else {
logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
throw new Error(`unexpected endlessXp mode: ${payload.Mode}`);
}
};
interface IEndlessXpRequest {
Mode: string; // "r"
Category: TEndlessXpCategory;
Choices: string[];
}
type IEndlessXpRequest =
| {
Mode: "r";
Category: TEndlessXpCategory;
Choices: string[];
}
| {
Mode: "c" | "something else";
Category: TEndlessXpCategory;
};
const generateRandomRewards = (deckName: string): ICountedStoreItem[] => {
const reward = getRandomElement(ExportRewards[deckName][0])!;
return [
{
StoreItem: reward.type,
ItemCount: reward.itemCount
}
];
};
const normalModeChosenRewards: Record<string, string[]> = {
Excalibur: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ExcaliburHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ExcaliburChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Excalibur/RadialJavelinAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ExcaliburSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ExcaliburBlueprint"
],
Trinity: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrinityHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrinityChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Trinity/EnergyVampireAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrinitySystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrinityBlueprint"
],
Ember: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/EmberHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/EmberChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Ember/WorldOnFireAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/EmberSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/EmberBlueprint"
],
Loki: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/LOKIHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/LOKIChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Loki/InvisibilityAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/LOKISystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/LOKIBlueprint"
],
Mag: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Mag/CrushAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagBlueprint"
],
Rhino: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RhinoHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RhinoChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Rhino/RhinoChargeAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RhinoSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RhinoBlueprint"
],
Ash: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/AshHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/AshChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Ninja/GlaiveAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/AshSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/AshBlueprint"
],
Frost: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FrostHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FrostChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Frost/IceShieldAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FrostSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FrostBlueprint"
],
Nyx: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NyxHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NyxChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Jade/SelfBulletAttractorAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NyxSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NyxBlueprint"
],
Saryn: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/SarynHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/SarynChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Saryn/PoisonAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/SarynSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/SarynBlueprint"
],
Vauban: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrapperHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrapperChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Trapper/LevTrapAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrapperSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrapperBlueprint"
],
Nova: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NovaHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NovaChassisBlueprint",
"/Lotus/StoreItems/Powersuits/AntiMatter/MolecularPrimeAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NovaSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NovaBlueprint"
],
Nekros: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NecroHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NecroChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Necro/CloneTheDeadAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NecroSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NecroBlueprint"
],
Valkyr: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BerserkerHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BerserkerChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Berserker/IntimidateAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BerserkerSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BerserkerBlueprint"
],
Oberon: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PaladinHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PaladinChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Paladin/RegenerationAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PaladinSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PaladinBlueprint"
],
Hydroid: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HydroidHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HydroidChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Pirate/CannonBarrageAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HydroidSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HydroidBlueprint"
],
Mirage: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HarlequinHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HarlequinChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Harlequin/LightAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HarlequinSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HarlequinBlueprint"
],
Limbo: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagicianHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagicianChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Magician/TearInSpaceAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagicianSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagicianBlueprint"
],
Mesa: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GunslingerHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GunslingerChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Cowgirl/GunFuPvPAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GunslingerSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GunslingerBlueprint"
],
Chroma: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ChromaHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ChromaChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Dragon/DragonLuckAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ChromaSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ChromaBlueprint"
],
Atlas: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BrawlerHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BrawlerChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Brawler/BrawlerPassiveAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BrawlerSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BrawlerBlueprint"
],
Ivara: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RangerHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RangerChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Ranger/RangerStealAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RangerSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RangerBlueprint"
],
Inaros: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MummyHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MummyChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Sandman/SandmanSwarmAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MummySystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MummyBlueprint"
],
Titania: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FairyHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FairyChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Fairy/FairyFlightAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FairySystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FairyBlueprint"
],
Nidus: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NidusHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NidusChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Infestation/InfestPodsAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NidusSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NidusBlueprint"
],
Octavia: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/OctaviaHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/OctaviaChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Bard/BardCharmAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/OctaviaSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/OctaviaBlueprint"
],
Harrow: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PriestHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PriestChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Priest/PriestPactAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PriestSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PriestBlueprint"
],
Gara: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GlassHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GlassChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Glass/GlassFragmentAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GlassSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GlassBlueprint"
],
Khora: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/KhoraHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/KhoraChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Khora/KhoraCrackAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/KhoraSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/KhoraBlueprint"
],
Revenant: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RevenantHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RevenantChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Revenant/RevenantMarkAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RevenantSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RevenantBlueprint"
],
Garuda: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GarudaHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GarudaChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Garuda/GarudaUnstoppableAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GarudaSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GarudaBlueprint"
],
Baruuk: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PacifistHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PacifistChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Pacifist/PacifistFistAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PacifistSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PacifistBlueprint"
],
Hildryn: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/IronframeHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/IronframeChassisBlueprint",
"/Lotus/StoreItems/Powersuits/IronFrame/IronFrameStripAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/IronframeSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/IronframeBlueprint"
]
};
const generateNormalModeRewards = (choices: string[]): IEndlessXpReward[] => {
const choiceRewards = normalModeChosenRewards[choices[0]];
return [
{
RequiredTotalXp: 190,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessNormalSilverRewards"
)
},
{
RequiredTotalXp: 400,
Rewards: [
{
StoreItem: choiceRewards[0],
ItemCount: 1
}
]
},
{
RequiredTotalXp: 630,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessNormalSilverRewards"
)
},
{
RequiredTotalXp: 890,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessNormalMODRewards"
)
},
{
RequiredTotalXp: 1190,
Rewards: [
{
StoreItem: choiceRewards[1],
ItemCount: 1
}
]
},
{
RequiredTotalXp: 1540,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessNormalGoldRewards"
)
},
{
RequiredTotalXp: 1950,
Rewards: [
{
StoreItem: choiceRewards[2],
ItemCount: 1
}
]
},
{
RequiredTotalXp: 2430,
Rewards: [
{
StoreItem: choiceRewards[3],
ItemCount: 1
}
]
},
{
RequiredTotalXp: 2990,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessNormalArcaneRewards"
)
},
{
RequiredTotalXp: 3640,
Rewards: [
{
StoreItem: choiceRewards[4],
ItemCount: 1
}
]
}
];
};
const hardModeChosenRewards: Record<string, string> = {
Braton: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/BratonIncarnonUnlocker",
Lato: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/LatoIncarnonUnlocker",
Skana: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/SkanaIncarnonUnlocker",
Paris: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/ParisIncarnonUnlocker",
Kunai: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/KunaiIncarnonUnlocker",
Boar: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/BoarIncarnonUnlocker",
Gammacor: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/GammacorIncarnonUnlocker",
Anku: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/AnkuIncarnonUnlocker",
Gorgon: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/GorgonIncarnonUnlocker",
Angstrum: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/AngstrumIncarnonUnlocker",
Bo: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/BoIncarnonUnlocker",
Latron: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/LatronIncarnonUnlocker",
Furis: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/FurisIncarnonUnlocker",
Furax: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/FuraxIncarnonUnlocker",
Strun: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/StrunIncarnonUnlocker",
Lex: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/LexIncarnonUnlocker",
Magistar: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/MagistarIncarnonUnlocker",
Boltor: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/BoltorIncarnonUnlocker",
Bronco: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/BroncoIncarnonUnlocker",
CeramicDagger: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/CeramicDaggerIncarnonUnlocker",
Torid: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/ToridIncarnonUnlocker",
DualToxocyst: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/DualToxocystIncarnonUnlocker",
DualIchor: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/DualIchorIncarnonUnlocker",
Miter: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/MiterIncarnonUnlocker",
Atomos: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/AtomosIncarnonUnlocker",
AckAndBrunt: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/AckAndBruntIncarnonUnlocker",
Soma: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/SomaIncarnonUnlocker",
Vasto: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/VastoIncarnonUnlocker",
NamiSolo: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/NamiSoloIncarnonUnlocker",
Burston: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/BurstonIncarnonUnlocker",
Zylok: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/ZylokIncarnonUnlocker",
Sibear: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/SibearIncarnonUnlocker",
Dread: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/DreadIncarnonUnlocker",
Despair: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/DespairIncarnonUnlocker",
Hate: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/HateIncarnonUnlocker",
Dera: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/DeraIncarnonUnlocker",
Cestra: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/CestraIncarnonUnlocker",
Okina: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/OkinaIncarnonUnlocker",
Sybaris: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/SybarisIncarnonUnlocker",
Sicarus: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/SicarusIncarnonUnlocker",
RivenPrimary: "/Lotus/StoreItems/Upgrades/Mods/Randomized/RawRifleRandomMod",
RivenSecondary: "/Lotus/StoreItems/Upgrades/Mods/Randomized/RawPistolRandomMod",
RivenMelee: "/Lotus/StoreItems/Upgrades/Mods/Randomized/RawMeleeRandomMod",
Kuva: "/Lotus/Types/Game/DuviriEndless/CircuitSteelPathBIGKuvaReward"
};
const generateHardModeRewards = (choices: string[]): IEndlessXpReward[] => {
return [
{
RequiredTotalXp: 285,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathSilverRewards"
)
},
{
RequiredTotalXp: 600,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathArcaneRewards"
)
},
{
RequiredTotalXp: 945,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathSilverRewards"
)
},
{
RequiredTotalXp: 1335,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathSilverRewards"
)
},
{
RequiredTotalXp: 1785,
Rewards: [
{
StoreItem: hardModeChosenRewards[choices[0]],
ItemCount: 1
}
]
},
{
RequiredTotalXp: 2310,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathGoldRewards"
)
},
{
RequiredTotalXp: 2925,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathGoldRewards"
)
},
{
RequiredTotalXp: 3645,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathArcaneRewards"
)
},
{
RequiredTotalXp: 4485,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathSteelEssenceRewards"
)
},
{
RequiredTotalXp: 5460,
Rewards: [
{
StoreItem: hardModeChosenRewards[choices[1]],
ItemCount: 1
}
]
}
];
};

View File

@ -30,15 +30,14 @@ export const fishmongerController: RequestHandler = async (req, res) => {
miscItemChanges.push({ ItemType: fish.ItemType, ItemCount: fish.ItemCount * -1 });
}
addMiscItems(inventory, miscItemChanges);
let affiliationMod;
if (gainedStanding && syndicateTag) affiliationMod = addStanding(inventory, syndicateTag, gainedStanding);
if (gainedStanding && syndicateTag) addStanding(inventory, syndicateTag, gainedStanding);
await inventory.save();
res.json({
InventoryChanges: {
MiscItems: miscItemChanges
},
SyndicateTag: syndicateTag,
StandingChange: affiliationMod?.Standing || 0
StandingChange: gainedStanding
});
};

View File

@ -64,7 +64,9 @@ export const focusController: RequestHandler = async (req, res) => {
}
);
res.end();
res.json({
FocusUpgrade: { ItemType: focusType }
});
break;
}
case FocusOperation.UnlockUpgrade: {

View File

@ -1,15 +1,54 @@
import { Request, Response } from "express";
import { toOid } from "@/src/helpers/inventoryHelpers";
import { Friendship } from "@/src/models/friendModel";
import { addAccountDataToFriendInfo, addInventoryDataToFriendInfo } from "@/src/services/friendService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IFriendInfo } from "@/src/types/friendTypes";
import { Request, RequestHandler, Response } from "express";
// POST with {} instead of GET as of 38.5.0
const getFriendsController = (_request: Request, response: Response): void => {
response.writeHead(200, {
//Connection: "keep-alive",
//"Content-Encoding": "gzip",
"Content-Type": "text/html",
// charset: "UTF - 8",
"Content-Length": "3"
});
response.end(Buffer.from([0x7b, 0x7d, 0x0a]));
export const getFriendsController: RequestHandler = async (req: Request, res: Response) => {
const accountId = await getAccountIdForRequest(req);
const response: IGetFriendsResponse = {
Current: [],
IncomingFriendRequests: [],
OutgoingFriendRequests: []
};
const [internalFriendships, externalFriendships] = await Promise.all([
Friendship.find({ owner: accountId }),
Friendship.find({ friend: accountId }, "owner Note")
]);
for (const externalFriendship of externalFriendships) {
if (!internalFriendships.find(x => x.friend.equals(externalFriendship.owner))) {
response.IncomingFriendRequests.push({
_id: toOid(externalFriendship.owner),
Note: externalFriendship.Note
});
}
}
for (const internalFriendship of internalFriendships) {
const friendInfo: IFriendInfo = {
_id: toOid(internalFriendship.friend)
};
if (externalFriendships.find(x => x.owner.equals(internalFriendship.friend))) {
response.Current.push(friendInfo);
} else {
response.OutgoingFriendRequests.push(friendInfo);
}
}
const promises: Promise<void>[] = [];
for (const arr of Object.values(response)) {
for (const friendInfo of arr) {
promises.push(addAccountDataToFriendInfo(friendInfo));
promises.push(addInventoryDataToFriendInfo(friendInfo));
}
}
await Promise.all(promises);
res.json(response);
};
export { getFriendsController };
// interface IGetFriendsResponse {
// Current: IFriendInfo[];
// IncomingFriendRequests: IFriendInfo[];
// OutgoingFriendRequests: IFriendInfo[];
// }
type IGetFriendsResponse = Record<"Current" | "IncomingFriendRequests" | "OutgoingFriendRequests", IFriendInfo[]>;

View File

@ -1,13 +1,13 @@
import { RequestHandler } from "express";
import { Guild } from "@/src/models/guildModel";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getAccountForRequest } from "@/src/services/loginService";
import { logger } from "@/src/utils/logger";
import { getInventory } from "@/src/services/inventoryService";
import { createUniqueClanName, getGuildClient } from "@/src/services/guildService";
export const getGuildController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "GuildId");
const account = await getAccountForRequest(req);
const inventory = await getInventory(account._id.toString(), "GuildId");
if (inventory.GuildId) {
const guild = await Guild.findById(inventory.GuildId);
if (guild) {
@ -24,7 +24,7 @@ export const getGuildController: RequestHandler = async (req, res) => {
guild.CeremonyResetDate = undefined;
await guild.save();
}
res.json(await getGuildClient(guild, accountId));
res.json(await getGuildClient(guild, account));
return;
}
}

View File

@ -2,6 +2,7 @@ import { RequestHandler } from "express";
import { Types } from "mongoose";
import { Guild } from "@/src/models/guildModel";
import { getDojoClient } from "@/src/services/guildService";
import { Account } from "@/src/models/loginModel";
export const getGuildDojoController: RequestHandler = async (req, res) => {
const guildId = req.query.guildId as string;
@ -25,7 +26,8 @@ export const getGuildDojoController: RequestHandler = async (req, res) => {
}
const payload: IGetGuildDojoRequest = req.body ? (JSON.parse(String(req.body)) as IGetGuildDojoRequest) : {};
res.json(await getDojoClient(guild, 0, payload.ComponentId));
const account = await Account.findById(req.query.accountId as string);
res.json(await getDojoClient(guild, 0, payload.ComponentId, account?.BuildLabel));
};
interface IGetGuildDojoRequest {

View File

@ -1,7 +1,7 @@
import { toOid } from "@/src/helpers/inventoryHelpers";
import { Account, Ignore } from "@/src/models/loginModel";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IFriendInfo } from "@/src/types/guildTypes";
import { IFriendInfo } from "@/src/types/friendTypes";
import { parallelForeach } from "@/src/utils/async-utils";
import { RequestHandler } from "express";

View File

@ -3,7 +3,6 @@ import { config } from "@/src/services/configService";
import allShipFeatures from "@/static/fixed_responses/allShipFeatures.json";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { createGarden, getPersonalRooms } from "@/src/services/personalRoomsService";
import { getShip } from "@/src/services/shipService";
import { toOid } from "@/src/helpers/inventoryHelpers";
import { IGetShipResponse } from "@/src/types/shipTypes";
import { IPersonalRoomsClient } from "@/src/types/personalRoomsTypes";
@ -21,7 +20,6 @@ export const getShipController: RequestHandler = async (req, res) => {
const personalRooms = personalRoomsDb.toJSON<IPersonalRoomsClient>();
const loadout = await getLoadout(accountId);
const ship = await getShip(personalRoomsDb.activeShipId, "ShipAttachments SkinFlavourItem");
const getShipResponse: IGetShipResponse = {
ShipOwnerId: accountId,
@ -31,8 +29,8 @@ export const getShipController: RequestHandler = async (req, res) => {
ShipId: toOid(personalRoomsDb.activeShipId),
ShipInterior: {
Colors: personalRooms.ShipInteriorColors,
ShipAttachments: ship.ShipAttachments,
SkinFlavourItem: ship.SkinFlavourItem
ShipAttachments: { HOOD_ORNAMENT: "" },
SkinFlavourItem: ""
},
FavouriteLoadoutId: personalRooms.Ship.FavouriteLoadoutId
? toOid(personalRooms.Ship.FavouriteLoadoutId)

View File

@ -1,14 +1,29 @@
import { RequestHandler } from "express";
import { getVendorManifestByTypeName } from "@/src/services/serversideVendorsService";
import { applyStandingToVendorManifest, getVendorManifestByTypeName } from "@/src/services/serversideVendorsService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { config } from "@/src/services/configService";
export const getVendorInfoController: RequestHandler = (req, res) => {
if (typeof req.query.vendor == "string") {
const manifest = getVendorManifestByTypeName(req.query.vendor);
if (!manifest) {
throw new Error(`Unknown vendor: ${req.query.vendor}`);
}
res.json(manifest);
} else {
res.status(400).end();
export const getVendorInfoController: RequestHandler = async (req, res) => {
let manifest = getVendorManifestByTypeName(req.query.vendor as string);
if (!manifest) {
throw new Error(`Unknown vendor: ${req.query.vendor as string}`);
}
// For testing purposes, authenticating with this endpoint is optional here, but would be required on live.
if (req.query.accountId) {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
manifest = applyStandingToVendorManifest(inventory, manifest);
if (config.dev?.keepVendorsExpired) {
manifest = {
VendorInfo: {
...manifest.VendorInfo,
Expiry: { $date: { $numberLong: "0" } }
}
};
}
}
res.json(manifest);
};

View File

@ -1,12 +1,19 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Account } from "@/src/models/loginModel";
import { areFriends } from "@/src/services/friendService";
import { createMessage } from "@/src/services/inboxService";
import { getInventory, updateCurrency } from "@/src/services/inventoryService";
import {
combineInventoryChanges,
getEffectiveAvatarImageType,
getInventory,
updateCurrency
} from "@/src/services/inventoryService";
import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { IOid } from "@/src/types/commonTypes";
import { IPurchaseParams } from "@/src/types/purchaseTypes";
import { IInventoryChanges, IPurchaseParams } from "@/src/types/purchaseTypes";
import { RequestHandler } from "express";
import { ExportFlavour } from "warframe-public-export-plus";
import { ExportBundles, ExportFlavour } from "warframe-public-export-plus";
export const giftingController: RequestHandler = async (req, res) => {
const data = getJSONfromString<IGiftingRequest>(String(req.body));
@ -30,8 +37,11 @@ export const giftingController: RequestHandler = async (req, res) => {
}
// Cannot gift to players who have gifting disabled.
// TODO: Also consider GIFT_MODE_FRIENDS once friends are implemented
if (inventory.Settings?.GiftMode == "GIFT_MODE_NONE") {
const senderAccount = await getAccountForRequest(req);
if (
inventory.Settings?.GiftMode == "GIFT_MODE_NONE" ||
(inventory.Settings?.GiftMode == "GIFT_MODE_FRIENDS" && !(await areFriends(account._id, senderAccount._id)))
) {
res.status(400).send("17").end();
return;
}
@ -40,11 +50,7 @@ export const giftingController: RequestHandler = async (req, res) => {
// TODO: Cannot gift archwing items to players that have not completed the archwing quest. (Code 7)
// TODO: Cannot gift necramechs to players that have not completed heart of deimos. (Code 20)
const senderAccount = await getAccountForRequest(req);
const senderInventory = await getInventory(
senderAccount._id.toString(),
"PremiumCredits PremiumCreditsFree ActiveAvatarImageType GiftsRemaining"
);
const senderInventory = await getInventory(senderAccount._id.toString());
if (senderInventory.GiftsRemaining == 0) {
res.status(400).send("10").end();
@ -52,7 +58,20 @@ export const giftingController: RequestHandler = async (req, res) => {
}
senderInventory.GiftsRemaining -= 1;
updateCurrency(senderInventory, data.PurchaseParams.ExpectedPrice, true);
const inventoryChanges: IInventoryChanges = updateCurrency(
senderInventory,
data.PurchaseParams.ExpectedPrice,
true
);
if (data.PurchaseParams.StoreItem in ExportBundles) {
const bundle = ExportBundles[data.PurchaseParams.StoreItem];
if (bundle.giftingBonus) {
combineInventoryChanges(
inventoryChanges,
(await handleStoreItemAcquisition(bundle.giftingBonus, senderInventory)).InventoryChanges
);
}
}
await senderInventory.save();
const senderName = getSuffixedName(senderAccount);
@ -71,7 +90,7 @@ export const giftingController: RequestHandler = async (req, res) => {
}
],
sub: "/Lotus/Language/Menu/GiftReceivedSubject",
icon: ExportFlavour[senderInventory.ActiveAvatarImageType].icon,
icon: ExportFlavour[getEffectiveAvatarImageType(senderInventory)].icon,
gifts: [
{
GiftType: data.PurchaseParams.StoreItem
@ -80,7 +99,9 @@ export const giftingController: RequestHandler = async (req, res) => {
}
]);
res.end();
res.json({
InventoryChanges: inventoryChanges
});
};
interface IGiftingRequest {

View File

@ -104,7 +104,7 @@ export const guildTechController: RequestHandler = async (req, res) => {
) {
throw new Error(`unexpected TechProductCategory: ${data.TechProductCategory}`);
}
if (!inventory[getSalvageCategory(data.TechProductCategory)].id(data.CategoryItemId)) {
if (!inventory[getSalvageCategory(data.TechProductCategory)].id(data.CategoryItemId!)) {
throw new Error(
`no item with id ${data.CategoryItemId} in ${getSalvageCategory(data.TechProductCategory)} array`
);

View File

@ -1,17 +1,24 @@
import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getAccountForRequest } from "@/src/services/loginService";
import { createNewSession } from "@/src/managers/sessionManager";
import { logger } from "@/src/utils/logger";
import { ISession } from "@/src/types/session";
import { JSONParse } from "json-with-bigint";
import { toOid2, version_compare } from "@/src/helpers/inventoryHelpers";
const hostSessionController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const hostSessionRequest = JSON.parse(req.body as string) as ISession;
const account = await getAccountForRequest(req);
const hostSessionRequest = JSONParse(String(req.body)) as ISession;
logger.debug("HostSession Request", { hostSessionRequest });
const session = createNewSession(hostSessionRequest, accountId);
const session = createNewSession(hostSessionRequest, account._id);
logger.debug(`New Session Created`, { session });
res.json({ sessionId: { $oid: session.sessionId }, rewardSeed: 99999999 });
if (account.BuildLabel && version_compare(account.BuildLabel, "2015.03.21.08.17") < 0) {
// U15 or below
res.send(session.sessionId.toString());
} else {
res.json({ sessionId: toOid2(session.sessionId, account.BuildLabel), rewardSeed: 99999999 });
}
};
export { hostSessionController };

View File

@ -9,7 +9,12 @@ import {
getMessage
} from "@/src/services/inboxService";
import { getAccountForRequest, getAccountFromSuffixedName, getSuffixedName } from "@/src/services/loginService";
import { addItems, combineInventoryChanges, getInventory } from "@/src/services/inventoryService";
import {
addItems,
combineInventoryChanges,
getEffectiveAvatarImageType,
getInventory
} from "@/src/services/inventoryService";
import { logger } from "@/src/utils/logger";
import { ExportFlavour } from "warframe-public-export-plus";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
@ -88,7 +93,7 @@ export const inboxController: RequestHandler = async (req, res) => {
}
],
sub: "/Lotus/Language/Menu/GiftReceivedConfirmationSubject",
icon: ExportFlavour[inventory.ActiveAvatarImageType].icon,
icon: ExportFlavour[getEffectiveAvatarImageType(inventory)].icon,
highPriority: true
}
]);

View File

@ -1,5 +1,5 @@
import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getAccountForRequest } from "@/src/services/loginService";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory, addMiscItems, updateCurrency, addRecipes, freeUpSlot } from "@/src/services/inventoryService";
import { IOid } from "@/src/types/commonTypes";
@ -12,7 +12,7 @@ import {
} from "@/src/types/inventoryTypes/inventoryTypes";
import { ExportMisc } from "warframe-public-export-plus";
import { getRecipe } from "@/src/services/itemDataService";
import { toMongoDate } from "@/src/helpers/inventoryHelpers";
import { toMongoDate, version_compare } from "@/src/helpers/inventoryHelpers";
import { logger } from "@/src/utils/logger";
import { colorToShard } from "@/src/helpers/shardHelper";
import { config } from "@/src/services/configService";
@ -23,12 +23,12 @@ import {
} from "@/src/services/infestedFoundryService";
export const infestedFoundryController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const account = await getAccountForRequest(req);
switch (req.query.mode) {
case "s": {
// shard installation
const request = getJSONfromString<IShardInstallRequest>(String(req.body));
const inventory = await getInventory(accountId);
const inventory = await getInventory(account._id.toString());
const suit = inventory.Suits.id(request.SuitId.$oid)!;
if (!suit.ArchonCrystalUpgrades || suit.ArchonCrystalUpgrades.length != 5) {
suit.ArchonCrystalUpgrades = [{}, {}, {}, {}, {}];
@ -56,7 +56,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
case "x": {
// shard removal
const request = getJSONfromString<IShardUninstallRequest>(String(req.body));
const inventory = await getInventory(accountId);
const inventory = await getInventory(account._id.toString());
const suit = inventory.Suits.id(request.SuitId.$oid)!;
const miscItemChanges: IMiscItem[] = [];
@ -70,19 +70,30 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
ItemCount: 1
});
addMiscItems(inventory, miscItemChanges);
// consume resources
if (!config.infiniteHelminthMaterials) {
let type: string;
let count: number;
if (account.BuildLabel && version_compare(account.BuildLabel, "2025.05.20.10.18") < 0) {
// < 38.6.0
type = "/Lotus/Types/Items/InfestedFoundry/HelminthBile";
count = 300;
} else {
// >= 38.6.0
type =
archonCrystalRemovalResource[
suit.ArchonCrystalUpgrades![request.Slot].Color!.replace("_MYTHIC", "")
];
count = suit.ArchonCrystalUpgrades![request.Slot].Color!.indexOf("_MYTHIC") != -1 ? 300 : 150;
}
inventory.InfestedFoundry!.Resources!.find(x => x.ItemType == type)!.Count -= count;
}
}
// remove from suit
suit.ArchonCrystalUpgrades![request.Slot] = {};
if (!config.infiniteHelminthMaterials) {
// remove bile
const bile = inventory.InfestedFoundry!.Resources!.find(
x => x.ItemType == "/Lotus/Types/Items/InfestedFoundry/HelminthBile"
)!;
bile.Count -= 300;
}
await inventory.save();
const infestedFoundry = inventory.toJSON<IInventoryClient>().InfestedFoundry!;
@ -99,7 +110,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
case "n": {
// name the beast
const request = getJSONfromString<IHelminthNameRequest>(String(req.body));
const inventory = await getInventory(accountId);
const inventory = await getInventory(account._id.toString());
inventory.InfestedFoundry ??= {};
inventory.InfestedFoundry.Name = request.newName;
await inventory.save();
@ -122,7 +133,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
}
const request = getJSONfromString<IHelminthFeedRequest>(String(req.body));
const inventory = await getInventory(accountId);
const inventory = await getInventory(account._id.toString());
inventory.InfestedFoundry ??= {};
inventory.InfestedFoundry.Resources ??= [];
@ -218,7 +229,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
case "o": {
// offerings update
const request = getJSONfromString<IHelminthOfferingsUpdate>(String(req.body));
const inventory = await getInventory(accountId);
const inventory = await getInventory(account._id.toString());
inventory.InfestedFoundry ??= {};
inventory.InfestedFoundry.InvigorationIndex = request.OfferingsIndex;
inventory.InfestedFoundry.InvigorationSuitOfferings = request.SuitTypes;
@ -239,7 +250,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
case "a": {
// subsume warframe
const request = getJSONfromString<IHelminthSubsumeRequest>(String(req.body));
const inventory = await getInventory(accountId);
const inventory = await getInventory(account._id.toString());
const recipe = getRecipe(request.Recipe)!;
if (!config.infiniteHelminthMaterials) {
for (const ingredient of recipe.secretIngredients!) {
@ -289,7 +300,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
case "r": {
// rush subsume
const inventory = await getInventory(accountId);
const inventory = await getInventory(account._id.toString());
const currencyChanges = updateCurrency(inventory, 50, true);
const recipeChanges = handleSubsumeCompletion(inventory);
await inventory.save();
@ -307,7 +318,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
case "u": {
const request = getJSONfromString<IHelminthInvigorationRequest>(String(req.body));
const inventory = await getInventory(accountId);
const inventory = await getInventory(account._id.toString());
const suit = inventory.Suits.id(request.SuitId.$oid)!;
const upgradesExpiry = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
suit.OffensiveUpgrade = request.OffensiveUpgradeType;
@ -340,7 +351,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
}
case "custom_unlockall": {
const inventory = await getInventory(accountId);
const inventory = await getInventory(account._id.toString());
inventory.InfestedFoundry ??= {};
inventory.InfestedFoundry.XP ??= 0;
if (151875_00 > inventory.InfestedFoundry.XP) {
@ -439,3 +450,12 @@ const apetiteModel = (x: number): number => {
}
return 3;
};
const archonCrystalRemovalResource: Record<string, string> = {
ACC_RED: "/Lotus/Types/Items/InfestedFoundry/HelminthOxides",
ACC_YELLOW: "/Lotus/Types/Items/InfestedFoundry/HelminthBile",
ACC_BLUE: "/Lotus/Types/Items/InfestedFoundry/HelminthSynthetics",
ACC_GREEN: "/Lotus/Types/Items/InfestedFoundry/HelminthBiotics",
ACC_ORANGE: "/Lotus/Types/Items/InfestedFoundry/HelminthPheromones",
ACC_PURPLE: "/Lotus/Types/Items/InfestedFoundry/HelminthCalx"
};

View File

@ -24,7 +24,11 @@ import {
import { logger } from "@/src/utils/logger";
import { catBreadHash } from "@/src/helpers/stringHelpers";
import { Types } from "mongoose";
import { isNemesisCompatibleWithVersion } from "@/src/helpers/nemesisHelpers";
import { getNemesisManifest } from "@/src/helpers/nemesisHelpers";
import { getPersonalRooms } from "@/src/services/personalRoomsService";
import { IPersonalRoomsClient } from "@/src/types/personalRoomsTypes";
import { Ship } from "@/src/models/shipModel";
import { toLegacyOid, version_compare } from "@/src/helpers/inventoryHelpers";
export const inventoryController: RequestHandler = async (request, response) => {
const account = await getAccountForRequest(request);
@ -130,13 +134,12 @@ export const getInventoryResponse = async (
xpBasedLevelCapDisabled: boolean,
buildLabel: string | undefined
): Promise<IInventoryClient> => {
const inventoryWithLoadOutPresets = await inventory.populate<{ LoadOutPresets: ILoadoutDatabase }>(
"LoadOutPresets"
);
const inventoryWithLoadOutPresetsAndShips = await inventoryWithLoadOutPresets.populate<{ Ships: IShipInventory }>(
"Ships"
);
const inventoryResponse = inventoryWithLoadOutPresetsAndShips.toJSON<IInventoryClient>();
const [inventoryWithLoadOutPresets, ships] = await Promise.all([
inventory.populate<{ LoadOutPresets: ILoadoutDatabase }>("LoadOutPresets"),
Ship.find({ ShipOwnerId: inventory.accountOwnerId })
]);
const inventoryResponse = inventoryWithLoadOutPresets.toJSON<IInventoryClient>();
inventoryResponse.Ships = ships.map(x => x.toJSON<IShipInventory>());
if (config.infiniteCredits) {
inventoryResponse.RegularCredits = 999999999;
@ -303,20 +306,44 @@ export const getInventoryResponse = async (
// Set 2FA enabled so trading post can be used
inventoryResponse.HWIDProtectEnabled = true;
// Fix nemesis for older versions
if (
inventoryResponse.Nemesis &&
buildLabel &&
!isNemesisCompatibleWithVersion(inventoryResponse.Nemesis, buildLabel)
) {
inventoryResponse.Nemesis = undefined;
if (buildLabel) {
// Fix nemesis for older versions
if (
inventoryResponse.Nemesis &&
version_compare(buildLabel, getNemesisManifest(inventoryResponse.Nemesis.manifest).minBuild) < 0
) {
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, "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);
}
}
}
}
}
return inventoryResponse;
};
const addString = (arr: string[], str: string): void => {
if (!arr.find(x => x == str)) {
if (arr.indexOf(str) == -1) {
arr.push(str);
}
};

View File

@ -7,7 +7,7 @@ import { Account } from "@/src/models/loginModel";
import { createAccount, isCorrectPassword, isNameTaken } from "@/src/services/loginService";
import { IDatabaseAccountJson, ILoginRequest, ILoginResponse } from "@/src/types/loginTypes";
import { logger } from "@/src/utils/logger";
import { version_compare } from "@/src/services/worldStateService";
import { version_compare } from "@/src/helpers/inventoryHelpers";
export const loginController: RequestHandler = async (request, response) => {
const loginRequest = JSON.parse(String(request.body)) as ILoginRequest; // parse octet stream of json data to json object
@ -20,7 +20,21 @@ export const loginController: RequestHandler = async (request, response) => {
? request.query.buildLabel.split(" ").join("+")
: buildConfig.buildLabel;
const myAddress = request.host.indexOf("warframe.com") == -1 ? request.host : config.myAddress;
let myAddress: string;
let myUrlBase: string = request.protocol + "://";
if (request.host.indexOf("warframe.com") == -1) {
// Client request was redirected cleanly, so we know it can reach us how it's reaching us now.
myAddress = request.hostname;
myUrlBase += request.host;
} else {
// Don't know how the client reached us, hoping the config does.
myAddress = config.myAddress;
myUrlBase += myAddress;
const port: number = request.protocol == "http" ? config.httpPort || 80 : config.httpsPort || 443;
if (port != (request.protocol == "http" ? 80 : 443)) {
myUrlBase += ":" + port;
}
}
if (
!account &&
@ -41,17 +55,18 @@ export const loginController: RequestHandler = async (request, response) => {
email: loginRequest.email,
password: loginRequest.password,
DisplayName: name,
CountryCode: loginRequest.lang.toUpperCase(),
CountryCode: loginRequest.lang?.toUpperCase() ?? "EN",
ClientType: loginRequest.ClientType == "webui-register" ? "webui" : loginRequest.ClientType,
CrossPlatformAllowed: true,
ForceLogoutVersion: 0,
ConsentNeeded: false,
TrackedSettings: [],
Nonce: nonce,
BuildLabel: buildLabel
BuildLabel: buildLabel,
LastLogin: new Date()
});
logger.debug("created new account");
response.json(createLoginResponse(myAddress, newAccount, buildLabel));
response.json(createLoginResponse(myAddress, myUrlBase, newAccount, buildLabel));
return;
} catch (error: unknown) {
if (error instanceof Error) {
@ -82,21 +97,30 @@ export const loginController: RequestHandler = async (request, response) => {
}
} else {
if (account.Nonce && account.ClientType != "webui" && !account.Dropped && !loginRequest.kick) {
response.status(400).json({ error: "nonce still set" });
return;
// U17 seems to handle "nonce still set" like a login failure.
if (version_compare(buildLabel, "2015.12.05.18.07") >= 0) {
response.status(400).send({ error: "nonce still set" });
return;
}
}
account.ClientType = loginRequest.ClientType;
account.Nonce = nonce;
account.CountryCode = loginRequest.lang.toUpperCase();
account.CountryCode = loginRequest.lang?.toUpperCase() ?? "EN";
account.BuildLabel = buildLabel;
account.LastLogin = new Date();
}
await account.save();
response.json(createLoginResponse(myAddress, account.toJSON(), buildLabel));
response.json(createLoginResponse(myAddress, myUrlBase, account.toJSON(), buildLabel));
};
const createLoginResponse = (myAddress: string, account: IDatabaseAccountJson, buildLabel: string): ILoginResponse => {
const createLoginResponse = (
myAddress: string,
myUrlBase: string,
account: IDatabaseAccountJson,
buildLabel: string
): ILoginResponse => {
const resp: ILoginResponse = {
id: account.id,
DisplayName: account.DisplayName,
@ -104,10 +128,15 @@ const createLoginResponse = (myAddress: string, account: IDatabaseAccountJson, b
AmazonAuthToken: account.AmazonAuthToken,
AmazonRefreshToken: account.AmazonRefreshToken,
Nonce: account.Nonce,
IRC: config.myIrcAddresses ?? [myAddress],
NRS: config.NRS,
BuildLabel: buildLabel
};
if (version_compare(buildLabel, "2015.02.13.10.41") >= 0) {
resp.NRS = config.NRS;
}
if (version_compare(buildLabel, "2015.05.14.16.29") >= 0) {
// U17 and up
resp.IRC = config.myIrcAddresses ?? [myAddress];
}
if (version_compare(buildLabel, "2018.11.08.14.45") >= 0) {
// U24 and up
resp.ConsentNeeded = account.ConsentNeeded;
@ -129,11 +158,11 @@ const createLoginResponse = (myAddress: string, account: IDatabaseAccountJson, b
}
if (version_compare(buildLabel, "2022.09.06.19.24") >= 0) {
resp.CrossPlatformAllowed = account.CrossPlatformAllowed;
resp.HUB = `https://${myAddress}/api/`;
resp.HUB = `${myUrlBase}/api/`;
resp.MatchmakingBuildId = buildConfig.matchmakingBuildId;
}
if (version_compare(buildLabel, "2023.04.25.23.40") >= 0) {
resp.platformCDNs = [`https://${myAddress}/`];
resp.platformCDNs = [`${myUrlBase}/`];
}
return resp;
};

View File

@ -26,7 +26,7 @@ export const loginRewardsSelectionController: RequestHandler = async (req, res)
StoreItemType: body.ChosenReward
};
inventoryChanges = (await handleStoreItemAcquisition(body.ChosenReward, inventory)).InventoryChanges;
if (!evergreenRewards.find(x => x == body.ChosenReward)) {
if (evergreenRewards.indexOf(body.ChosenReward) == -1) {
inventory.LoginMilestoneRewards.push(body.ChosenReward);
}
} else {

View File

@ -57,11 +57,15 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
const firstCompletion = missionReport.SortieId
? inventory.CompletedSorties.indexOf(missionReport.SortieId) == -1
: false;
const inventoryUpdates = await addMissionInventoryUpdates(inventory, missionReport);
const inventoryUpdates = await addMissionInventoryUpdates(account, inventory, missionReport);
if (
missionReport.MissionStatus !== "GS_SUCCESS" &&
!(missionReport.RewardInfo?.jobId || missionReport.RewardInfo?.challengeMissionId)
!(
missionReport.RewardInfo?.jobId ||
missionReport.RewardInfo?.challengeMissionId ||
missionReport.RewardInfo?.T
)
) {
if (missionReport.EndOfMatchUpload) {
inventory.RewardSeed = generateRewardSeed();

View File

@ -2,7 +2,7 @@ import { RequestHandler } from "express";
import { ExportWeapons } from "warframe-public-export-plus";
import { IMongoDate } from "@/src/types/commonTypes";
import { toMongoDate } from "@/src/helpers/inventoryHelpers";
import { CRng } from "@/src/services/rngService";
import { SRng } from "@/src/services/rngService";
import { ArtifactPolarity, EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import {
@ -140,7 +140,7 @@ const getModularWeaponSale = (
partTypes: string[],
getItemType: (parts: string[]) => string
): IModularWeaponSaleInfo => {
const rng = new CRng(day);
const rng = new SRng(day);
const parts = partTypes.map(partType => rng.randomElement(partTypeToParts[partType])!);
let partsCost = 0;
for (const part of parts) {

View File

@ -1,18 +1,24 @@
import { version_compare } from "@/src/helpers/inventoryHelpers";
import {
consumeModCharge,
decodeNemesisGuess,
encodeNemesisGuess,
getInfNodes,
getKnifeUpgrade,
getNemesisManifest,
getNemesisPasscode,
getNemesisPasscodeModTypes,
getWeaponsForManifest,
IKnifeResponse,
showdownNodes
GUESS_CORRECT,
GUESS_INCORRECT,
GUESS_NEUTRAL,
GUESS_NONE,
GUESS_WILDCARD,
IKnifeResponse
} from "@/src/helpers/nemesisHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Loadout } from "@/src/models/inventoryModels/loadoutModel";
import { freeUpSlot, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getAccountForRequest } from "@/src/services/loginService";
import { SRng } from "@/src/services/rngService";
import { IMongoDate, IOid } from "@/src/types/commonTypes";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
@ -24,16 +30,17 @@ import {
IUpgradeClient,
IWeaponSkinClient,
LoadoutIndex,
TEquipmentKey
TEquipmentKey,
TNemesisFaction
} from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express";
export const nemesisController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const account = await getAccountForRequest(req);
if ((req.query.mode as string) == "f") {
const body = getJSONfromString<IValenceFusionRequest>(String(req.body));
const inventory = await getInventory(accountId, body.Category + " WeaponBin");
const inventory = await getInventory(account._id.toString(), body.Category + " WeaponBin");
const destWeapon = inventory[body.Category].id(body.DestWeapon.$oid)!;
const sourceWeapon = inventory[body.Category].id(body.SourceWeapon.$oid)!;
const destFingerprint = JSON.parse(destWeapon.UpgradeFingerprint!) as IInnateDamageFingerprint;
@ -68,7 +75,7 @@ export const nemesisController: RequestHandler = async (req, res) => {
}
});
} else if ((req.query.mode as string) == "p") {
const inventory = await getInventory(accountId, "Nemesis");
const inventory = await getInventory(account._id.toString(), "Nemesis");
const body = getJSONfromString<INemesisPrespawnCheckRequest>(String(req.body));
const passcode = getNemesisPasscode(inventory.Nemesis!);
let guessResult = 0;
@ -81,7 +88,7 @@ export const nemesisController: RequestHandler = async (req, res) => {
}
} else {
for (let i = 0; i != 3; ++i) {
if (body.guess[i] == passcode[i]) {
if (body.guess[i] == passcode[i] || body.guess[i] == GUESS_WILDCARD) {
++guessResult;
}
}
@ -89,25 +96,36 @@ export const nemesisController: RequestHandler = async (req, res) => {
res.json({ GuessResult: guessResult });
} else if (req.query.mode == "r") {
const inventory = await getInventory(
accountId,
account._id.toString(),
"Nemesis LoadOutPresets CurrentLoadOutIds DataKnives Upgrades RawUpgrades"
);
const body = getJSONfromString<INemesisRequiemRequest>(String(req.body));
if (inventory.Nemesis!.Faction == "FC_INFESTATION") {
const guess: number[] = [body.guess & 0xf, (body.guess >> 4) & 0xf, (body.guess >> 8) & 0xf];
const passcode = getNemesisPasscode(inventory.Nemesis!)[0];
// Add to GuessHistory
const result1 = passcode == guess[0] ? 0 : 1;
const result2 = passcode == guess[1] ? 0 : 1;
const result3 = passcode == guess[2] ? 0 : 1;
const result1 = passcode == guess[0] ? GUESS_CORRECT : GUESS_INCORRECT;
const result2 = passcode == guess[1] ? GUESS_CORRECT : GUESS_INCORRECT;
const result3 = passcode == guess[2] ? GUESS_CORRECT : GUESS_INCORRECT;
inventory.Nemesis!.GuessHistory.push(
encodeNemesisGuess(guess[0], result1, guess[1], result2, guess[2], result3)
encodeNemesisGuess([
{
symbol: guess[0],
result: result1
},
{
symbol: guess[1],
result: result2
},
{
symbol: guess[2],
result: result3
}
])
);
// Increase antivirus if correct antivirus mod is installed
const response: IKnifeResponse = {};
if (result1 == 0 || result2 == 0 || result3 == 0) {
if (result1 == GUESS_CORRECT || result2 == GUESS_CORRECT || result3 == GUESS_CORRECT) {
let antivirusGain = 5;
const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!;
const dataknifeLoadout = loadout.DATAKNIFE.id(inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE].$oid);
@ -143,44 +161,89 @@ export const nemesisController: RequestHandler = async (req, res) => {
if (inventory.Nemesis!.HenchmenKilled >= 100) {
inventory.Nemesis!.HenchmenKilled = 100;
}
inventory.Nemesis!.InfNodes = getInfNodes("FC_INFESTATION", 0);
inventory.Nemesis!.InfNodes = getInfNodes(getNemesisManifest(inventory.Nemesis!.manifest), 0);
await inventory.save();
res.json(response);
} else {
const passcode = getNemesisPasscode(inventory.Nemesis!);
if (passcode[body.position] != body.guess) {
res.end();
} else {
inventory.Nemesis!.Rank += 1;
inventory.Nemesis!.InfNodes = getInfNodes(inventory.Nemesis!.Faction, inventory.Nemesis!.Rank);
await inventory.save();
res.json({ RankIncrease: 1 });
// For first guess, create a new entry.
if (body.position == 0) {
inventory.Nemesis!.GuessHistory.push(
encodeNemesisGuess([
{
symbol: GUESS_NONE,
result: GUESS_NEUTRAL
},
{
symbol: GUESS_NONE,
result: GUESS_NEUTRAL
},
{
symbol: GUESS_NONE,
result: GUESS_NEUTRAL
}
])
);
}
// Evaluate guess
const correct =
body.guess == GUESS_WILDCARD || getNemesisPasscode(inventory.Nemesis!)[body.position] == body.guess;
// Update entry
const guess = decodeNemesisGuess(
inventory.Nemesis!.GuessHistory[inventory.Nemesis!.GuessHistory.length - 1]
);
guess[body.position].symbol = body.guess;
guess[body.position].result = correct ? GUESS_CORRECT : GUESS_INCORRECT;
inventory.Nemesis!.GuessHistory[inventory.Nemesis!.GuessHistory.length - 1] = encodeNemesisGuess(guess);
// Increase rank if incorrect
let RankIncrease: number | undefined;
if (!correct) {
RankIncrease = 1;
const manifest = getNemesisManifest(inventory.Nemesis!.manifest);
inventory.Nemesis!.Rank = Math.min(inventory.Nemesis!.Rank + 1, manifest.systemIndexes.length - 1);
inventory.Nemesis!.InfNodes = getInfNodes(manifest, inventory.Nemesis!.Rank);
}
await inventory.save();
res.json({ RankIncrease });
}
} else if ((req.query.mode as string) == "rs") {
// report spawn; POST but no application data in body
const inventory = await getInventory(accountId, "Nemesis");
const inventory = await getInventory(account._id.toString(), "Nemesis");
inventory.Nemesis!.LastEnc = inventory.Nemesis!.MissionCount;
await inventory.save();
res.json({ LastEnc: inventory.Nemesis!.LastEnc });
} else if ((req.query.mode as string) == "s") {
const inventory = await getInventory(accountId, "Nemesis");
const inventory = await getInventory(account._id.toString(), "Nemesis");
if (inventory.Nemesis) {
logger.warn(`overwriting an existing nemesis as a new one is being requested`);
}
const body = getJSONfromString<INemesisStartRequest>(String(req.body));
body.target.fp = BigInt(body.target.fp);
const manifest = getNemesisManifest(body.target.manifest);
if (account.BuildLabel && version_compare(account.BuildLabel, manifest.minBuild) < 0) {
logger.warn(
`client on version ${account.BuildLabel} provided nemesis manifest ${body.target.manifest} which was expected to require ${manifest.minBuild} or above. please file a bug report.`
);
}
let weaponIdx = -1;
if (body.target.Faction != "FC_INFESTATION") {
const weapons = getWeaponsForManifest(body.target.manifest);
const weapons: readonly string[] = manifest.weapons;
const initialWeaponIdx = new SRng(body.target.fp).randomInt(0, weapons.length - 1);
weaponIdx = initialWeaponIdx;
do {
const weapon = weapons[weaponIdx];
if (!body.target.DisallowedWeapons.find(x => x == weapon)) {
break;
}
weaponIdx = (weaponIdx + 1) % weapons.length;
} while (weaponIdx != initialWeaponIdx);
if (body.target.DisallowedWeapons) {
do {
const weapon = weapons[weaponIdx];
if (body.target.DisallowedWeapons.indexOf(weapon) == -1) {
break;
}
weaponIdx = (weaponIdx + 1) % weapons.length;
} while (weaponIdx != initialWeaponIdx);
}
}
inventory.Nemesis = {
@ -197,14 +260,14 @@ export const nemesisController: RequestHandler = async (req, res) => {
k: false,
Traded: false,
d: new Date(),
InfNodes: getInfNodes(body.target.Faction, 0),
InfNodes: getInfNodes(manifest, 0),
GuessHistory: [],
Hints: [],
HintProgress: 0,
Weakened: body.target.Weakened,
Weakened: false,
PrevOwners: 0,
HenchmenKilled: 0,
SecondInCommand: body.target.SecondInCommand,
SecondInCommand: false,
MissionCount: 0,
LastEnc: 0
};
@ -215,14 +278,14 @@ export const nemesisController: RequestHandler = async (req, res) => {
});
} else if ((req.query.mode as string) == "w") {
const inventory = await getInventory(
accountId,
account._id.toString(),
"Nemesis LoadOutPresets CurrentLoadOutIds DataKnives Upgrades RawUpgrades"
);
//const body = getJSONfromString<INemesisWeakenRequest>(String(req.body));
inventory.Nemesis!.InfNodes = [
{
Node: showdownNodes[inventory.Nemesis!.Faction],
Node: getNemesisManifest(inventory.Nemesis!.manifest).showdownNode,
Influence: 1
}
];
@ -265,11 +328,11 @@ interface INemesisStartRequest {
KillingSuit: string;
killingDamageType: number;
ShoulderHelmet: string;
DisallowedWeapons: string[];
DisallowedWeapons?: string[];
WeaponIdx: number;
AgentIdx: number;
BirthNode: string;
Faction: string;
Faction: TNemesisFaction;
Rank: number;
k: boolean;
Traded: boolean;

View File

@ -2,13 +2,16 @@ import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { addMiscItems, getInventory } from "@/src/services/inventoryService";
import { ExportRelics, IRelic } from "warframe-public-export-plus";
import { config } from "@/src/services/configService";
export const projectionManagerController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const request = JSON.parse(String(req.body)) as IProjectionUpgradeRequest;
const [era, category, currentQuality] = parseProjection(request.projectionType);
const upgradeCost = (request.qualityTag - qualityKeywordToNumber[currentQuality]) * 25;
const upgradeCost = config.dontSubtractVoidTraces
? 0
: (request.qualityTag - qualityKeywordToNumber[currentQuality]) * 25;
const newProjectionType = findProjection(era, category, qualityNumberToKeyword[request.qualityTag]);
addMiscItems(inventory, [
{

View File

@ -0,0 +1,25 @@
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
// Basic shim handling action=sync to login on U21
export const questControlController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const quests: IQuestState[] = [];
for (const quest of inventory.QuestKeys) {
quests.push({
quest: quest.ItemType,
state: 3 // COMPLETE
});
}
res.json({
QuestState: quests
});
};
interface IQuestState {
quest: string;
state: number;
task?: string;
}

View File

@ -0,0 +1,99 @@
import { toOid } from "@/src/helpers/inventoryHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Friendship } from "@/src/models/friendModel";
import { Account } from "@/src/models/loginModel";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IOid } from "@/src/types/commonTypes";
import { parallelForeach } from "@/src/utils/async-utils";
import { RequestHandler } from "express";
import { Types } from "mongoose";
export const removeFriendGetController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
if (req.query.all) {
const [internalFriendships, externalFriendships] = await Promise.all([
Friendship.find({ owner: accountId }, "friend"),
Friendship.find({ friend: accountId }, "owner")
]);
const promises: Promise<void>[] = [];
const friends: IOid[] = [];
for (const externalFriendship of externalFriendships) {
if (!internalFriendships.find(x => x.friend.equals(externalFriendship.owner))) {
promises.push(Friendship.deleteOne({ _id: externalFriendship._id }) as unknown as Promise<void>);
friends.push(toOid(externalFriendship.owner));
}
}
await Promise.all(promises);
res.json({
Friends: friends
} satisfies IRemoveFriendsResponse);
} else {
const friendId = req.query.friendId as string;
await Promise.all([
Friendship.deleteOne({ owner: accountId, friend: friendId }),
Friendship.deleteOne({ owner: friendId, friend: accountId })
]);
res.json({
Friends: [{ $oid: friendId }]
} satisfies IRemoveFriendsResponse);
}
};
export const removeFriendPostController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const data = getJSONfromString<IBatchRemoveFriendsRequest>(String(req.body));
const friends = new Set((await Friendship.find({ owner: accountId }, "friend")).map(x => x.friend));
// TOVERIFY: Should pending friendships also be kept?
// Keep friends that have been online within threshold
await parallelForeach([...friends], async friend => {
const account = (await Account.findById(friend, "LastLogin"))!;
const daysLoggedOut = (Date.now() - account.LastLogin.getTime()) / 86400_000;
if (daysLoggedOut < data.DaysLoggedOut) {
friends.delete(friend);
}
});
if (data.SkipClanmates) {
const inventory = await getInventory(accountId, "GuildId");
if (inventory.GuildId) {
await parallelForeach([...friends], async friend => {
const friendInventory = await getInventory(friend.toString(), "GuildId");
if (friendInventory.GuildId?.equals(inventory.GuildId)) {
friends.delete(friend);
}
});
}
}
// Remove all remaining friends that aren't in SkipFriendIds & give response.
const promises = [];
const response: IOid[] = [];
for (const friend of friends) {
if (!data.SkipFriendIds.find(skipFriendId => checkFriendId(skipFriendId, friend))) {
promises.push(Friendship.deleteOne({ owner: accountId, friend: friend }));
promises.push(Friendship.deleteOne({ owner: friend, friend: accountId }));
response.push(toOid(friend));
}
}
await Promise.all(promises);
res.json({
Friends: response
} satisfies IRemoveFriendsResponse);
};
// The friend ids format is a bit weird, e.g. when 6633b81e9dba0b714f28ff02 (A) is friends with 67cdac105ef1f4b49741c267 (B), A's friend id for B is 808000105ef1f40560ca079e and B's friend id for A is 8000b81e9dba0b06408a8075.
const checkFriendId = (friendId: string, b: Types.ObjectId): boolean => {
return friendId.substring(6, 6 + 8) == b.toString().substring(6, 6 + 8);
};
interface IBatchRemoveFriendsRequest {
DaysLoggedOut: number;
SkipClanmates: boolean;
SkipFriendIds: string[];
}
interface IRemoveFriendsResponse {
Friends: IOid[];
}

View File

@ -0,0 +1,23 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory, updateCurrency } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const renamePetController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "KubrowPets PremiumCredits PremiumCreditsFree");
const data = getJSONfromString<IRenamePetRequest>(String(req.body));
const details = inventory.KubrowPets.id(data.petId)!.Details!;
details.Name = data.name;
const currencyChanges = updateCurrency(inventory, 15, true);
await inventory.save();
res.json({
...data,
inventoryChanges: currencyChanges
});
};
interface IRenamePetRequest {
petId: string;
name: string;
}

View File

@ -1,8 +1,7 @@
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { config } from "@/src/services/configService";
import { addEmailItem, getInventory, updateCurrency } from "@/src/services/inventoryService";
import { addEmailItem, getDialogue, getInventory, updateCurrency } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { ICompletedDialogue, IDialogueDatabase } from "@/src/types/inventoryTypes/inventoryTypes";
import { ICompletedDialogue } from "@/src/types/inventoryTypes/inventoryTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { RequestHandler } from "express";
@ -107,26 +106,3 @@ interface IOtherDialogueInfo {
Tag: string;
Value: number;
}
const getDialogue = (inventory: TInventoryDatabaseDocument, dialogueName: string): IDialogueDatabase => {
let dialogue = inventory.DialogueHistory!.Dialogues!.find(x => x.DialogueName == dialogueName);
if (!dialogue) {
dialogue =
inventory.DialogueHistory!.Dialogues![
inventory.DialogueHistory!.Dialogues!.push({
Rank: 0,
Chemistry: 0,
AvailableDate: new Date(0),
AvailableGiftDate: new Date(0),
RankUpExpiry: new Date(0),
BountyChemExpiry: new Date(0),
QueuedDialogues: [],
Gifts: [],
Booleans: [],
Completed: [],
DialogueName: dialogueName
}) - 1
];
}
return dialogue;
};

View File

@ -2,11 +2,12 @@ import { RequestHandler } from "express";
import { ISaveLoadoutRequest } from "@/src/types/saveLoadoutTypes";
import { handleInventoryItemConfigChange } from "@/src/services/saveLoadoutService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
export const saveLoadoutController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const body: ISaveLoadoutRequest = JSON.parse(req.body as string) as ISaveLoadoutRequest;
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

View File

@ -45,6 +45,9 @@ export const sellController: RequestHandler = async (req, res) => {
if (payload.Items.SpaceGuns || payload.Items.SpaceMelee) {
requiredFields.add(InventorySlot.SPACEWEAPONS);
}
if (payload.Items.MechSuits) {
requiredFields.add(InventorySlot.MECHSUITS);
}
if (payload.Items.Sentinels || payload.Items.SentinelWeapons || payload.Items.MoaPets) {
requiredFields.add(InventorySlot.SENTINELS);
}
@ -136,6 +139,12 @@ export const sellController: RequestHandler = async (req, res) => {
freeUpSlot(inventory, InventorySlot.SPACEWEAPONS);
});
}
if (payload.Items.MechSuits) {
payload.Items.MechSuits.forEach(sellItem => {
inventory.MechSuits.pull({ _id: sellItem.String });
freeUpSlot(inventory, InventorySlot.MECHSUITS);
});
}
if (payload.Items.Sentinels) {
payload.Items.Sentinels.forEach(sellItem => {
inventory.Sentinels.pull({ _id: sellItem.String });
@ -285,6 +294,7 @@ interface ISellRequest {
SpaceSuits?: ISellItem[];
SpaceGuns?: ISellItem[];
SpaceMelee?: ISellItem[];
MechSuits?: ISellItem[];
Sentinels?: ISellItem[];
SentinelWeapons?: ISellItem[];
MoaPets?: ISellItem[];

View File

@ -13,7 +13,7 @@ export const setDojoComponentSettingsController: RequestHandler = async (req, re
res.json({ DojoRequestStatus: -1 });
return;
}
const component = guild.DojoComponents.id(req.query.componentId)!;
const component = guild.DojoComponents.id(req.query.componentId as string)!;
const data = getJSONfromString<ISetDojoComponentSettingsRequest>(String(req.body));
component.Settings = data.Settings;
await guild.save();

View File

@ -0,0 +1,30 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Friendship } from "@/src/models/friendModel";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const setFriendNoteController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const payload = getJSONfromString<ISetFriendNoteRequest>(String(req.body));
const friendship = await Friendship.findOne({ owner: accountId, friend: payload.FriendId }, "Note Favorite");
if (friendship) {
if ("Note" in payload) {
friendship.Note = payload.Note;
} else {
friendship.Favorite = payload.Favorite;
}
await friendship.save();
}
res.json({
Id: payload.FriendId,
SetNote: "Note" in payload,
Note: friendship?.Note,
Favorite: friendship?.Favorite
});
};
interface ISetFriendNoteRequest {
FriendId: string;
Note?: string;
Favorite?: boolean;
}

View File

@ -1,8 +1,8 @@
import { version_compare } from "@/src/helpers/inventoryHelpers";
import { Alliance, Guild, GuildMember } from "@/src/models/guildModel";
import { hasGuildPermissionEx } from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService";
import { version_compare } from "@/src/services/worldStateService";
import { GuildPermission, ILongMOTD } from "@/src/types/guildTypes";
import { RequestHandler } from "express";

View File

@ -11,7 +11,7 @@ import {
import { Types } from "mongoose";
import { ExportDojoRecipes } from "warframe-public-export-plus";
import { config } from "@/src/services/configService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getAccountForRequest } from "@/src/services/loginService";
import { getInventory } from "@/src/services/inventoryService";
interface IStartDojoRecipeRequest {
@ -20,10 +20,10 @@ interface IStartDojoRecipeRequest {
}
export const startDojoRecipeController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "GuildId LevelKeys");
const account = await getAccountForRequest(req);
const inventory = await getInventory(account._id.toString(), "GuildId LevelKeys");
const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Architect))) {
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, account._id, GuildPermission.Architect))) {
res.json({ DojoRequestStatus: -1 });
return;
}
@ -64,5 +64,5 @@ export const startDojoRecipeController: RequestHandler = async (req, res) => {
setDojoRoomLogFunded(guild, component);
}
await guild.save();
res.json(await getDojoClient(guild, 0));
res.json(await getDojoClient(guild, 0, undefined, account.BuildLabel));
};

View File

@ -3,12 +3,14 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express";
import { getRecipe } from "@/src/services/itemDataService";
import { addItem, freeUpSlot, getInventory, updateCurrency } from "@/src/services/inventoryService";
import { addItem, addKubrowPet, freeUpSlot, getInventory, updateCurrency } from "@/src/services/inventoryService";
import { unixTimesInMs } from "@/src/constants/timeConstants";
import { Types } from "mongoose";
import { InventorySlot, ISpectreLoadout } from "@/src/types/inventoryTypes/inventoryTypes";
import { toOid } from "@/src/helpers/inventoryHelpers";
import { fromOid, toOid } from "@/src/helpers/inventoryHelpers";
import { ExportWeapons } from "warframe-public-export-plus";
import { getRandomElement } from "@/src/services/rngService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
interface IStartRecipeRequest {
RecipeName: string;
@ -41,25 +43,36 @@ export const startRecipeController: RequestHandler = async (req, res) => {
];
for (let i = 0; i != recipe.ingredients.length; ++i) {
if (startRecipeRequest.Ids[i]) {
const category = ExportWeapons[recipe.ingredients[i].ItemType].productCategory;
if (category != "LongGuns" && category != "Pistols" && category != "Melee") {
throw new Error(`unexpected equipment ingredient type: ${category}`);
if (startRecipeRequest.Ids[i] && startRecipeRequest.Ids[i][0] != "/") {
if (recipe.ingredients[i].ItemType == "/Lotus/Types/Game/KubrowPet/Eggs/KubrowPetEggItem") {
const index = inventory.KubrowPetEggs!.findIndex(x => x._id.equals(startRecipeRequest.Ids[i]));
if (index != -1) {
inventory.KubrowPetEggs!.splice(index, 1);
}
} else {
const category = ExportWeapons[recipe.ingredients[i].ItemType].productCategory;
if (category != "LongGuns" && category != "Pistols" && category != "Melee") {
throw new Error(`unexpected equipment ingredient type: ${category}`);
}
const equipmentIndex = inventory[category].findIndex(x => x._id.equals(startRecipeRequest.Ids[i]));
if (equipmentIndex == -1) {
throw new Error(`could not find equipment item to use for recipe`);
}
pr[category] ??= [];
pr[category].push(inventory[category][equipmentIndex]);
inventory[category].splice(equipmentIndex, 1);
freeUpSlot(inventory, InventorySlot.WEAPONS);
}
const equipmentIndex = inventory[category].findIndex(x => x._id.equals(startRecipeRequest.Ids[i]));
if (equipmentIndex == -1) {
throw new Error(`could not find equipment item to use for recipe`);
}
pr[category] ??= [];
pr[category].push(inventory[category][equipmentIndex]);
inventory[category].splice(equipmentIndex, 1);
freeUpSlot(inventory, InventorySlot.WEAPONS);
} else {
await addItem(inventory, recipe.ingredients[i].ItemType, recipe.ingredients[i].ItemCount * -1);
}
}
if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") {
let inventoryChanges: IInventoryChanges | undefined;
if (recipe.secretIngredientAction == "SIA_CREATE_KUBROW") {
inventoryChanges = addKubrowPet(inventory, getRandomElement(recipe.secretIngredients!)!.ItemType);
pr.KubrowPet = new Types.ObjectId(fromOid(inventoryChanges.KubrowPets![0].ItemId));
} else if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") {
const spectreLoadout: ISpectreLoadout = {
ItemType: recipe.resultType,
Suits: "",
@ -116,5 +129,5 @@ export const startRecipeController: RequestHandler = async (req, res) => {
await inventory.save();
res.json({ RecipeId: toOid(pr._id) });
res.json({ RecipeId: toOid(pr._id), InventoryChanges: inventoryChanges });
};

View File

@ -1,15 +1,13 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { ExportNightwave, ExportSyndicates, ISyndicateSacrifice } from "warframe-public-export-plus";
import { ExportSyndicates, ISyndicateSacrifice } from "warframe-public-export-plus";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { addMiscItems, combineInventoryChanges, getInventory, updateCurrency } from "@/src/services/inventoryService";
import { addMiscItem, combineInventoryChanges, getInventory, updateCurrency } from "@/src/services/inventoryService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { isStoreItem, toStoreItem } from "@/src/services/itemDataService";
import { toStoreItem } from "@/src/services/itemDataService";
import { logger } from "@/src/utils/logger";
const nightwaveCredsItemType = ExportNightwave.rewards[ExportNightwave.rewards.length - 1].uniqueName;
export const syndicateSacrificeController: RequestHandler = async (request, response) => {
const accountId = await getAccountIdForRequest(request);
const inventory = await getInventory(accountId);
@ -20,74 +18,83 @@ export const syndicateSacrificeController: RequestHandler = async (request, resp
syndicate = inventory.Affiliations[inventory.Affiliations.push({ Tag: data.AffiliationTag, Standing: 0 }) - 1];
}
const level = data.SacrificeLevel - (syndicate.Title ?? 0);
const oldLevel = syndicate.Title ?? 0;
const levelIncrease = data.SacrificeLevel - oldLevel;
if (levelIncrease < 0) {
throw new Error(`syndicate sacrifice can not decrease level`);
}
if (levelIncrease > 1 && !data.AllowMultiple) {
throw new Error(`desired syndicate level is an increase of ${levelIncrease}, max. allowed increase is 1`);
}
const res: ISyndicateSacrificeResponse = {
AffiliationTag: data.AffiliationTag,
InventoryChanges: {},
Level: data.SacrificeLevel,
LevelIncrease: level <= 0 ? 1 : level,
LevelIncrease: data.SacrificeLevel < 0 ? 1 : levelIncrease,
NewEpisodeReward: false
};
// Process sacrifices and rewards for every level we're reaching
const manifest = ExportSyndicates[data.AffiliationTag];
let sacrifice: ISyndicateSacrifice | undefined;
let reward: string | undefined;
if (data.SacrificeLevel == 0) {
sacrifice = manifest.initiationSacrifice;
reward = manifest.initiationReward;
syndicate.Initiated = true;
} else {
sacrifice = manifest.titles?.find(x => x.level == data.SacrificeLevel)?.sacrifice;
}
for (let level = oldLevel + Math.min(levelIncrease, 1); level <= data.SacrificeLevel; ++level) {
let sacrifice: ISyndicateSacrifice | undefined;
if (level == 0) {
sacrifice = manifest.initiationSacrifice;
if (manifest.initiationReward) {
combineInventoryChanges(
res.InventoryChanges,
(await handleStoreItemAcquisition(manifest.initiationReward, inventory)).InventoryChanges
);
}
syndicate.Initiated = true;
} else {
sacrifice = manifest.titles?.find(x => x.level == level)?.sacrifice;
}
if (sacrifice) {
res.InventoryChanges = { ...updateCurrency(inventory, sacrifice.credits, false) };
if (sacrifice) {
updateCurrency(inventory, sacrifice.credits, false, res.InventoryChanges);
const miscItemChanges = sacrifice.items.map(x => ({
ItemType: x.ItemType,
ItemCount: x.ItemCount * -1
}));
addMiscItems(inventory, miscItemChanges);
res.InventoryChanges.MiscItems = miscItemChanges;
}
for (const item of sacrifice.items) {
addMiscItem(inventory, item.ItemType, item.ItemCount * -1, res.InventoryChanges);
}
}
syndicate.Title ??= 0;
syndicate.Title += 1;
if (syndicate.Title > 0 && manifest.favours.find(x => x.rankUpReward && x.requiredLevel == syndicate.Title)) {
syndicate.FreeFavorsEarned ??= [];
if (!syndicate.FreeFavorsEarned.includes(syndicate.Title)) {
syndicate.FreeFavorsEarned.push(syndicate.Title);
}
}
if (reward) {
combineInventoryChanges(
res.InventoryChanges,
(await handleStoreItemAcquisition(reward, inventory)).InventoryChanges
);
}
if (data.AffiliationTag == ExportNightwave.affiliationTag) {
const index = syndicate.Title - 1;
if (index < ExportNightwave.rewards.length) {
res.NewEpisodeReward = true;
const reward = ExportNightwave.rewards[index];
let rewardType = reward.uniqueName;
if (!isStoreItem(rewardType)) {
rewardType = toStoreItem(rewardType);
}
const rewardInventoryChanges = (await handleStoreItemAcquisition(rewardType, inventory, reward.itemCount))
.InventoryChanges;
if (Object.keys(rewardInventoryChanges).length == 0) {
logger.debug(`nightwave rank up reward did not seem to get added, giving 50 creds instead`);
rewardInventoryChanges.MiscItems = [{ ItemType: nightwaveCredsItemType, ItemCount: 50 }];
addMiscItems(inventory, rewardInventoryChanges.MiscItems);
}
combineInventoryChanges(res.InventoryChanges, rewardInventoryChanges);
// Quacks like a nightwave syndicate?
if (manifest.dailyChallenges) {
const title = manifest.titles!.find(x => x.level == level);
if (title) {
res.NewEpisodeReward = true;
let rewardType: string;
let rewardCount: number;
if (title.storeItemReward) {
rewardType = title.storeItemReward;
rewardCount = 1;
} else {
rewardType = toStoreItem(title.reward!.ItemType);
rewardCount = title.reward!.ItemCount;
}
const rewardInventoryChanges = (await handleStoreItemAcquisition(rewardType, inventory, rewardCount))
.InventoryChanges;
if (Object.keys(rewardInventoryChanges).length == 0) {
logger.debug(`nightwave rank up reward did not seem to get added, giving 50 creds instead`);
const nightwaveCredsItemType = manifest.titles![0].reward!.ItemType;
addMiscItem(inventory, nightwaveCredsItemType, 50, rewardInventoryChanges);
}
combineInventoryChanges(res.InventoryChanges, rewardInventoryChanges);
}
} else {
if (level > 0 && manifest.favours.find(x => x.rankUpReward && x.requiredLevel == level)) {
syndicate.FreeFavorsEarned ??= [];
if (!syndicate.FreeFavorsEarned.includes(level)) {
syndicate.FreeFavorsEarned.push(level);
}
}
}
}
// Commit
syndicate.Title = data.SacrificeLevel < 0 ? data.SacrificeLevel + 1 : data.SacrificeLevel;
await inventory.save();
response.json(res);

View File

@ -5,7 +5,7 @@ import { IMiscItem, InventorySlot } from "@/src/types/inventoryTypes/inventoryTy
import { IOid } from "@/src/types/commonTypes";
import { ExportSyndicates, ExportWeapons } from "warframe-public-export-plus";
import { logger } from "@/src/utils/logger";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { IAffiliationMods, IInventoryChanges } from "@/src/types/purchaseTypes";
import { EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes";
export const syndicateStandingBonusController: RequestHandler = async (req, res) => {
@ -54,13 +54,14 @@ export const syndicateStandingBonusController: RequestHandler = async (req, res)
inventoryChanges[slotBin] = { count: -1, platinum: 0, Slots: 1 };
}
const affiliationMod = addStanding(inventory, request.Operation.AffiliationTag, gainedStanding, true);
const affiliationMods: IAffiliationMods[] = [];
addStanding(inventory, request.Operation.AffiliationTag, gainedStanding, affiliationMods, true);
await inventory.save();
res.json({
InventoryChanges: inventoryChanges,
AffiliationMods: [affiliationMod]
AffiliationMods: affiliationMods
});
};

View File

@ -35,6 +35,17 @@ const trainingResultController: RequestHandler = async (req, res): Promise<void>
inventory.PlayerLevel += 1;
inventory.TradesRemaining += 1;
if (inventory.PlayerLevel == 2) {
await createMessage(accountId, [
{
sndr: "/Lotus/Language/Game/Maroo",
msg: "/Lotus/Language/Clan/MarooClanSearchDesc",
sub: "/Lotus/Language/Clan/MarooClanSearchTitle",
icon: "/Lotus/Interface/Icons/Npcs/Maroo.png"
}
]);
}
await createMessage(accountId, [
{
sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",

View File

@ -1,18 +1,26 @@
import { RequestHandler } from "express";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getAccountForRequest } from "@/src/services/loginService";
import { addChallenges, getInventory } from "@/src/services/inventoryService";
import { IChallengeProgress, ISeasonChallenge } from "@/src/types/inventoryTypes/inventoryTypes";
import { IAffiliationMods } from "@/src/types/purchaseTypes";
export const updateChallengeProgressController: RequestHandler = async (req, res) => {
const challenges = getJSONfromString<IUpdateChallengeProgressRequest>(String(req.body));
const accountId = await getAccountIdForRequest(req);
const account = await getAccountForRequest(req);
const inventory = await getInventory(accountId, "ChallengeProgress SeasonChallengeHistory Affiliations");
const inventory = await getInventory(
account._id.toString(),
"ChallengeProgress SeasonChallengeHistory Affiliations"
);
let affiliationMods: IAffiliationMods[] = [];
if (challenges.ChallengeProgress) {
affiliationMods = addChallenges(inventory, challenges.ChallengeProgress, challenges.SeasonChallengeCompletions);
affiliationMods = addChallenges(
account,
inventory,
challenges.ChallengeProgress,
challenges.SeasonChallengeCompletions
);
}
if (challenges.SeasonChallengeHistory) {
challenges.SeasonChallengeHistory.forEach(({ challenge, id }) => {

View File

@ -1,5 +1,6 @@
import { applyClientEquipmentUpdates, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IOid } from "@/src/types/commonTypes";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express";
@ -11,7 +12,7 @@ export const addXpController: RequestHandler = async (req, res) => {
const request = req.body as IAddXpRequest;
for (const [category, gear] of Object.entries(request)) {
for (const clientItem of gear) {
const dbItem = inventory[category as TEquipmentKey].id(clientItem.ItemId.$oid);
const dbItem = inventory[category as TEquipmentKey].id((clientItem.ItemId as IOid).$oid);
if (dbItem) {
if (dbItem.ItemType in ExportMisc.uniqueLevelCaps) {
if ((dbItem.Polarized ?? 0) < 5) {

View File

@ -10,6 +10,7 @@ import { Stats } from "@/src/models/statsModel";
import { GuildMember } from "@/src/models/guildModel";
import { Leaderboard } from "@/src/models/leaderboardModel";
import { deleteGuild } from "@/src/services/guildService";
import { Friendship } from "@/src/models/friendModel";
export const deleteAccountController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
@ -22,6 +23,8 @@ export const deleteAccountController: RequestHandler = async (req, res) => {
await Promise.all([
Account.deleteOne({ _id: accountId }),
Friendship.deleteMany({ owner: accountId }),
Friendship.deleteMany({ friend: accountId }),
GuildMember.deleteMany({ accountId: accountId }),
Ignore.deleteMany({ ignorer: accountId }),
Ignore.deleteMany({ ignoree: accountId }),

View File

@ -1,15 +1,18 @@
import { AllianceMember, Guild, GuildMember } from "@/src/models/guildModel";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountForRequest, isAdministrator } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const getAccountInfoController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req);
const inventory = await getInventory(account._id.toString(), "QuestKeys");
const info: IAccountInfo = {
DisplayName: account.DisplayName
DisplayName: account.DisplayName,
IsAdministrator: isAdministrator(account),
CompletedVorsPrize: !!inventory.QuestKeys.find(
x => x.ItemType == "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain"
)?.Completed
};
if (isAdministrator(account)) {
info.IsAdministrator = true;
}
const guildMember = await GuildMember.findOne({ accountId: account._id, status: 0 }, "guildId rank");
if (guildMember) {
const guild = (await Guild.findById(guildMember.guildId, "Ranks AllianceId"))!;
@ -31,7 +34,8 @@ export const getAccountInfoController: RequestHandler = async (req, res) => {
interface IAccountInfo {
DisplayName: string;
IsAdministrator?: boolean;
IsAdministrator: boolean;
CompletedVorsPrize: boolean;
GuildId?: string;
GuildPermissions?: number;
GuildRank?: number;

View File

@ -3,6 +3,7 @@ import { getDict, getItemName, getString } from "@/src/services/itemDataService"
import {
ExportArcanes,
ExportAvionics,
ExportBoosters,
ExportCustoms,
ExportDrones,
ExportGear,
@ -19,12 +20,12 @@ import {
ExportWeapons,
TRelicQuality
} from "warframe-public-export-plus";
import archonCrystalUpgrades from "@/static/fixed_responses/webuiArchonCrystalUpgrades.json";
import allIncarnons from "@/static/fixed_responses/allIncarnonList.json";
interface ListedItem {
uniqueName: string;
name: string;
subtype?: string;
fusionLimit?: number;
exalted?: string[];
badReason?: "starter" | "frivolous" | "notraw";
@ -34,7 +35,6 @@ interface ListedItem {
}
interface ItemLists {
archonCrystalUpgrades: Record<string, string>;
uniqueLevelCaps: Record<string, number>;
Suits: ListedItem[];
LongGuns: ListedItem[];
@ -54,6 +54,7 @@ interface ItemLists {
KubrowPets: ListedItem[];
EvolutionProgress: ListedItem[];
mods: ListedItem[];
Boosters: ListedItem[];
}
const relicQualitySuffixes: Record<TRelicQuality, string> = {
@ -66,7 +67,6 @@ const relicQualitySuffixes: Record<TRelicQuality, string> = {
const getItemListsController: RequestHandler = (req, response) => {
const lang = getDict(typeof req.query.lang == "string" ? req.query.lang : "en");
const res: ItemLists = {
archonCrystalUpgrades,
uniqueLevelCaps: ExportMisc.uniqueLevelCaps,
Suits: [],
LongGuns: [],
@ -85,7 +85,8 @@ const getItemListsController: RequestHandler = (req, response) => {
QuestKeys: [],
KubrowPets: [],
EvolutionProgress: [],
mods: []
mods: [],
Boosters: []
};
for (const [uniqueName, item] of Object.entries(ExportWarframes)) {
res[item.productCategory].push({
@ -175,7 +176,8 @@ const getItemListsController: RequestHandler = (req, response) => {
) {
res.miscitems.push({
uniqueName: uniqueName,
name: name
name: name,
subtype: "Resource"
});
}
}
@ -193,7 +195,8 @@ const getItemListsController: RequestHandler = (req, response) => {
for (const [uniqueName, item] of Object.entries(ExportGear)) {
res.miscitems.push({
uniqueName: uniqueName,
name: getString(item.name, lang)
name: getString(item.name, lang),
subtype: "Gear"
});
}
const recipeNameTemplate = getString("/Lotus/Language/Items/BlueprintAndItem", lang);
@ -293,6 +296,13 @@ const getItemListsController: RequestHandler = (req, response) => {
});
}
for (const item of Object.values(ExportBoosters)) {
res.Boosters.push({
uniqueName: item.typeName,
name: getString(item.name, lang)
});
}
response.json(res);
};

View File

@ -0,0 +1,45 @@
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory } from "@/src/services/inventoryService";
import { RequestHandler } from "express";
import { ExportBoosters } from "warframe-public-export-plus";
const I32_MAX = 0x7fffffff;
export const setBoosterController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const requests = req.body as { ItemType: string; ExpiryDate: number }[];
const inventory = await getInventory(accountId, "Boosters");
const boosters = inventory.Boosters;
if (
requests.some(request => {
if (typeof request.ItemType !== "string") return true;
if (Object.entries(ExportBoosters).find(([_, item]) => item.typeName === request.ItemType) === undefined)
return true;
if (typeof request.ExpiryDate !== "number") return true;
if (request.ExpiryDate < 0 || request.ExpiryDate > I32_MAX) return true;
return false;
})
) {
res.status(400).send("Invalid ItemType provided.");
return;
}
const now = Math.floor(Date.now() / 1000);
for (const { ItemType, ExpiryDate } of requests) {
if (ExpiryDate < now) {
// remove expired boosters
const index = boosters.findIndex(item => item.ItemType === ItemType);
if (index !== -1) {
boosters.splice(index, 1);
}
} else {
const boosterItem = boosters.find(item => item.ItemType === ItemType);
if (boosterItem) {
boosterItem.ExpiryDate = ExpiryDate;
} else {
boosters.push({ ItemType, ExpiryDate });
}
}
}
await inventory.save();
res.end();
};

View File

@ -84,7 +84,10 @@ export const getProfileViewingDataGetController: RequestHandler = async (req, re
res.status(409).send("Could not find requested account");
}
} else if (req.query.guildId) {
const guild = await Guild.findById(req.query.guildId, "Name Tier XP Class Emblem TechProjects ClaimedXP");
const guild = await Guild.findById(
req.query.guildId as string,
"Name Tier XP Class Emblem TechProjects ClaimedXP"
);
if (!guild) {
res.status(409).send("Could not find guild");
return;

View File

@ -48,7 +48,8 @@ const toDatabaseAccount = (createAccount: IAccountCreation): IDatabaseAccountReq
CrossPlatformAllowed: true,
ForceLogoutVersion: 0,
TrackedSettings: [],
Nonce: 0
Nonce: 0,
LastLogin: new Date()
} satisfies IDatabaseAccountRequiredFields;
};

View File

@ -1,9 +1,46 @@
import { IMongoDate, IOid } from "@/src/types/commonTypes";
import { IMongoDate, IOid, IOidWithLegacySupport } from "@/src/types/commonTypes";
import { Types } from "mongoose";
import { TRarity } from "warframe-public-export-plus";
export const version_compare = (a: string, b: string): number => {
const a_digits = a
.split("/")[0]
.split(".")
.map(x => parseInt(x));
const b_digits = b
.split("/")[0]
.split(".")
.map(x => parseInt(x));
for (let i = 0; i != a_digits.length; ++i) {
if (a_digits[i] != b_digits[i]) {
return a_digits[i] > b_digits[i] ? 1 : -1;
}
}
return 0;
};
export const toOid = (objectId: Types.ObjectId): IOid => {
return { $oid: objectId.toString() } satisfies IOid;
return { $oid: objectId.toString() };
};
export function toOid2(objectId: Types.ObjectId, buildLabel: undefined): IOid;
export function toOid2(objectId: Types.ObjectId, buildLabel: string | undefined): IOidWithLegacySupport;
export function toOid2(objectId: Types.ObjectId, buildLabel: string | undefined): IOidWithLegacySupport {
if (buildLabel && version_compare(buildLabel, "2016.12.21.19.13") <= 0) {
return { $id: objectId.toString() };
}
return { $oid: objectId.toString() };
}
export const toLegacyOid = (oid: IOidWithLegacySupport): void => {
if (!("$id" in oid)) {
oid.$id = oid.$oid;
delete oid.$oid;
}
};
export const fromOid = (oid: IOidWithLegacySupport): string => {
return (oid.$oid ?? oid.$id)!;
};
export const toMongoDate = (date: Date): IMongoDate => {

View File

@ -1,18 +1,203 @@
import { ExportRegions, ExportWarframes } from "warframe-public-export-plus";
import { IInfNode, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes";
import { IInfNode, TNemesisFaction } from "@/src/types/inventoryTypes/inventoryTypes";
import { getRewardAtPercentage, SRng } from "@/src/services/rngService";
import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel";
import { logger } from "../utils/logger";
import { IOid } from "../types/commonTypes";
import { Types } from "mongoose";
import { addMods, generateRewardSeed } from "../services/inventoryService";
import { isArchwingMission, version_compare } from "../services/worldStateService";
import { fromStoreItem, toStoreItem } from "../services/itemDataService";
import { createMessage } from "../services/inboxService";
import { isArchwingMission } from "../services/worldStateService";
export const getInfNodes = (faction: string, rank: number): IInfNode[] => {
type TInnateDamageTag =
| "InnateElectricityDamage"
| "InnateHeatDamage"
| "InnateFreezeDamage"
| "InnateToxinDamage"
| "InnateMagDamage"
| "InnateRadDamage"
| "InnateImpactDamage";
export interface INemesisManifest {
weapons: readonly string[];
systemIndexes: readonly number[];
showdownNode: string;
ephemeraChance: number;
ephemeraTypes?: Record<TInnateDamageTag, string>;
firstKillReward: string;
firstConvertReward: string;
messageTitle: string;
messageBody: string;
minBuild: string;
}
class KuvaLichManifest implements INemesisManifest {
weapons = [
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Drakgoon/KuvaDrakgoon",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Karak/KuvaKarak",
"/Lotus/Weapons/Grineer/Melee/GrnKuvaLichScythe/GrnKuvaLichScytheWeapon",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Kohm/KuvaKohm",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Ogris/KuvaOgris",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Quartakk/KuvaQuartakk",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Tonkor/KuvaTonkor",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Brakk/KuvaBrakk",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Kraken/KuvaKraken",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Seer/KuvaSeer",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Stubba/KuvaStubba",
"/Lotus/Weapons/Grineer/HeavyWeapons/GrnHeavyGrenadeLauncher",
"/Lotus/Weapons/Grineer/LongGuns/GrnKuvaLichRifle/GrnKuvaLichRifleWeapon"
];
systemIndexes = [2, 3, 9, 11, 18];
showdownNode = "CrewBattleNode557";
ephemeraChance = 0.05;
ephemeraTypes = {
InnateElectricityDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaLightningEphemera",
InnateHeatDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaFireEphemera",
InnateFreezeDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaIceEphemera",
InnateToxinDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaToxinEphemera",
InnateMagDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaMagneticEphemera",
InnateRadDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaTricksterEphemera",
InnateImpactDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaImpactEphemera"
};
firstKillReward = "/Lotus/StoreItems/Upgrades/Skins/Clan/LichKillerBadgeItem";
firstConvertReward = "/Lotus/StoreItems/Upgrades/Skins/Sigils/KuvaLichSigil";
messageTitle = "/Lotus/Language/Inbox/VanquishKuvaMsgTitle";
messageBody = "/Lotus/Language/Inbox/VanquishLichMsgBody";
minBuild = "2019.10.31.22.42"; // 26.0.0
}
class KuvaLichManifestVersionTwo extends KuvaLichManifest {
constructor() {
super();
this.ephemeraChance = 0.1;
this.minBuild = "2020.03.05.16.06"; // Unsure about this one, so using the same value as in version three.
}
}
class KuvaLichManifestVersionThree extends KuvaLichManifestVersionTwo {
constructor() {
super();
this.weapons.push("/Lotus/Weapons/Grineer/Bows/GrnBow/GrnBowWeapon");
this.weapons.push("/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Hind/KuvaHind");
this.weapons.push("/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Nukor/KuvaNukor");
this.ephemeraChance = 0.2;
this.minBuild = "2020.03.05.16.06"; // This is 27.2.0, tho 27.1.0 should also recognise this.
}
}
class KuvaLichManifestVersionFour extends KuvaLichManifestVersionThree {
constructor() {
super();
this.minBuild = "2021.07.05.17.03"; // Unsure about this one, so using the same value as in version five.
}
}
class KuvaLichManifestVersionFive extends KuvaLichManifestVersionFour {
constructor() {
super();
this.weapons.push("/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Hek/KuvaHekWeapon");
this.weapons.push("/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Zarr/KuvaZarr");
this.weapons.push("/Lotus/Weapons/Grineer/KuvaLich/HeavyWeapons/Grattler/KuvaGrattler");
this.minBuild = "2021.07.05.17.03"; // 30.5.0
}
}
class KuvaLichManifestVersionSix extends KuvaLichManifestVersionFive {
constructor() {
super();
this.weapons.push("/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Sobek/KuvaSobek");
this.minBuild = "2024.05.15.11.07"; // 35.6.0
}
}
class LawyerManifest implements INemesisManifest {
weapons = [
"/Lotus/Weapons/Corpus/LongGuns/CrpBriefcaseLauncher/CrpBriefcaseLauncher",
"/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEArcaPlasmor/CrpBEArcaPlasmor",
"/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEFluxRifle/CrpBEFluxRifle",
"/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBETetra/CrpBETetra",
"/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBECycron/CrpBECycron",
"/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBEDetron/CrpBEDetron",
"/Lotus/Weapons/Corpus/Pistols/CrpIgniterPistol/CrpIgniterPistol",
"/Lotus/Weapons/Corpus/Pistols/CrpBriefcaseAkimbo/CrpBriefcaseAkimboPistol"
];
systemIndexes = [1, 15, 4, 7, 8];
showdownNode = "CrewBattleNode558";
ephemeraChance = 0.2;
ephemeraTypes = {
InnateElectricityDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraA",
InnateHeatDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraB",
InnateFreezeDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraC",
InnateToxinDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraD",
InnateMagDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraE",
InnateRadDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraF",
InnateImpactDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraG"
};
firstKillReward = "/Lotus/StoreItems/Upgrades/Skins/Clan/CorpusLichBadgeItem";
firstConvertReward = "/Lotus/StoreItems/Upgrades/Skins/Sigils/CorpusLichSigil";
messageTitle = "/Lotus/Language/Inbox/VanquishLawyerMsgTitle";
messageBody = "/Lotus/Language/Inbox/VanquishLichMsgBody";
minBuild = "2021.07.05.17.03"; // 30.5.0
}
class LawyerManifestVersionTwo extends LawyerManifest {
constructor() {
super();
this.weapons.push("/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBEPlinx/CrpBEPlinxWeapon");
this.minBuild = "2022.11.30.08.13"; // 32.2.0
}
}
class LawyerManifestVersionThree extends LawyerManifestVersionTwo {
constructor() {
super();
this.weapons.push("/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEGlaxion/CrpBEGlaxion");
this.minBuild = "2024.05.15.11.07"; // 35.6.0
}
}
class LawyerManifestVersionFour extends LawyerManifestVersionThree {
constructor() {
super();
this.minBuild = "2024.10.01.11.03"; // 37.0.0
}
}
class InfestedLichManfest implements INemesisManifest {
weapons = [];
systemIndexes = [23];
showdownNode = "CrewBattleNode559";
ephemeraChance = 0;
firstKillReward = "/Lotus/StoreItems/Upgrades/Skins/Sigils/InfLichVanquishedSigil";
firstConvertReward = "/Lotus/StoreItems/Upgrades/Skins/Sigils/InfLichConvertedSigil";
messageTitle = "/Lotus/Language/Inbox/VanquishBandMsgTitle";
messageBody = "/Lotus/Language/Inbox/VanquishBandMsgBody";
minBuild = "2025.03.18.09.51"; // 38.5.0
}
const nemesisManifests: Record<string, INemesisManifest> = {
"/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifest": new KuvaLichManifest(),
"/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionTwo": new KuvaLichManifestVersionTwo(),
"/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionThree": new KuvaLichManifestVersionThree(),
"/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionFour": new KuvaLichManifestVersionFour(),
"/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionFive": new KuvaLichManifestVersionFive(),
"/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionSix": new KuvaLichManifestVersionSix(),
"/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifest": new LawyerManifest(),
"/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionTwo": new LawyerManifestVersionTwo(),
"/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionThree": new LawyerManifestVersionThree(),
"/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionFour": new LawyerManifestVersionFour(),
"/Lotus/Types/Enemies/InfestedLich/InfestedLichManifest": new InfestedLichManfest()
};
export const getNemesisManifest = (manifest: string): INemesisManifest => {
if (manifest in nemesisManifests) {
return nemesisManifests[manifest];
}
throw new Error(`unknown nemesis manifest: ${manifest}`);
};
export const getInfNodes = (manifest: INemesisManifest, rank: number): IInfNode[] => {
const infNodes = [];
const systemIndex = systemIndexes[faction][rank];
const systemIndex = manifest.systemIndexes[rank];
for (const [key, value] of Object.entries(ExportRegions)) {
if (
value.systemIndex === systemIndex &&
@ -34,20 +219,8 @@ export const getInfNodes = (faction: string, rank: number): IInfNode[] => {
return infNodes;
};
const systemIndexes: Record<string, number[]> = {
FC_GRINEER: [2, 3, 9, 11, 18],
FC_CORPUS: [1, 15, 4, 7, 8],
FC_INFESTATION: [23]
};
export const showdownNodes: Record<string, string> = {
FC_GRINEER: "CrewBattleNode557",
FC_CORPUS: "CrewBattleNode558",
FC_INFESTATION: "CrewBattleNode559"
};
// Get a parazon 'passcode' based on the nemesis fingerprint so it's always the same for the same nemesis.
export const getNemesisPasscode = (nemesis: { fp: bigint; Faction: string }): number[] => {
export const getNemesisPasscode = (nemesis: { fp: bigint; Faction: TNemesisFaction }): number[] => {
const rng = new SRng(nemesis.fp);
const choices = [0, 1, 2, 3, 5, 6, 7];
let choiceIndex = rng.randomInt(0, choices.length - 1);
@ -64,7 +237,7 @@ export const getNemesisPasscode = (nemesis: { fp: bigint; Faction: string }): nu
return passcode;
};
const reqiuemMods: readonly string[] = [
const requiemMods: readonly string[] = [
"/Lotus/Upgrades/Mods/Immortal/ImmortalOneMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalTwoMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalThreeMod",
@ -86,33 +259,55 @@ const antivirusMods: readonly string[] = [
"/Lotus/Upgrades/Mods/Immortal/AntivirusEightMod"
];
export const getNemesisPasscodeModTypes = (nemesis: { fp: bigint; Faction: string }): string[] => {
export const getNemesisPasscodeModTypes = (nemesis: { fp: bigint; Faction: TNemesisFaction }): string[] => {
const passcode = getNemesisPasscode(nemesis);
return nemesis.Faction == "FC_INFESTATION"
? passcode.map(i => antivirusMods[i])
: passcode.map(i => reqiuemMods[i]);
: passcode.map(i => requiemMods[i]);
};
export const encodeNemesisGuess = (
symbol1: number,
result1: number,
symbol2: number,
result2: number,
symbol3: number,
result3: number
): number => {
// Symbols; 0-7 are the normal requiem mods.
export const GUESS_NONE = 8;
export const GUESS_WILDCARD = 9;
// Results; there are 3, 4, 5 as well which are more muted versions but unused afaik.
export const GUESS_NEUTRAL = 0;
export const GUESS_INCORRECT = 1;
export const GUESS_CORRECT = 2;
interface NemesisPositionGuess {
symbol: number;
result: number;
}
export type NemesisGuess = [NemesisPositionGuess, NemesisPositionGuess, NemesisPositionGuess];
export const encodeNemesisGuess = (guess: NemesisGuess): number => {
return (
(symbol1 & 0xf) |
((result1 & 3) << 12) |
((symbol2 << 4) & 0xff) |
((result2 << 14) & 0xffff) |
((symbol3 & 0xf) << 8) |
((result3 & 3) << 16)
(guess[0].symbol & 0xf) |
((guess[0].result & 3) << 12) |
((guess[1].symbol << 4) & 0xff) |
((guess[1].result << 14) & 0xffff) |
((guess[2].symbol & 0xf) << 8) |
((guess[2].result & 3) << 16)
);
};
export const decodeNemesisGuess = (val: number): number[] => {
return [val & 0xf, (val >> 12) & 3, (val & 0xff) >> 4, (val & 0xffff) >> 14, (val >> 8) & 0xf, (val >> 16) & 3];
export const decodeNemesisGuess = (val: number): NemesisGuess => {
return [
{
symbol: val & 0xf,
result: (val >> 12) & 3
},
{
symbol: (val & 0xff) >> 4,
result: (val & 0xffff) >> 14
},
{
symbol: (val >> 8) & 0xf,
result: (val >> 16) & 3
}
];
};
export interface IKnifeResponse {
@ -199,92 +394,31 @@ export const consumeModCharge = (
}
};
const kuvaLichVersionSixWeapons = [
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Drakgoon/KuvaDrakgoon",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Karak/KuvaKarak",
"/Lotus/Weapons/Grineer/Melee/GrnKuvaLichScythe/GrnKuvaLichScytheWeapon",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Kohm/KuvaKohm",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Ogris/KuvaOgris",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Quartakk/KuvaQuartakk",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Tonkor/KuvaTonkor",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Brakk/KuvaBrakk",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Kraken/KuvaKraken",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Seer/KuvaSeer",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Stubba/KuvaStubba",
"/Lotus/Weapons/Grineer/HeavyWeapons/GrnHeavyGrenadeLauncher",
"/Lotus/Weapons/Grineer/LongGuns/GrnKuvaLichRifle/GrnKuvaLichRifleWeapon",
"/Lotus/Weapons/Grineer/Bows/GrnBow/GrnBowWeapon",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Hind/KuvaHind",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Nukor/KuvaNukor",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Hek/KuvaHekWeapon",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Zarr/KuvaZarr",
"/Lotus/Weapons/Grineer/KuvaLich/HeavyWeapons/Grattler/KuvaGrattler",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Sobek/KuvaSobek"
];
const corpusVersionThreeWeapons = [
"/Lotus/Weapons/Corpus/LongGuns/CrpBriefcaseLauncher/CrpBriefcaseLauncher",
"/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEArcaPlasmor/CrpBEArcaPlasmor",
"/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEFluxRifle/CrpBEFluxRifle",
"/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBETetra/CrpBETetra",
"/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBECycron/CrpBECycron",
"/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBEDetron/CrpBEDetron",
"/Lotus/Weapons/Corpus/Pistols/CrpIgniterPistol/CrpIgniterPistol",
"/Lotus/Weapons/Corpus/Pistols/CrpBriefcaseAkimbo/CrpBriefcaseAkimboPistol",
"/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBEPlinx/CrpBEPlinxWeapon",
"/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEGlaxion/CrpBEGlaxion"
];
export const getWeaponsForManifest = (manifest: string): readonly string[] => {
switch (manifest) {
case "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionSix": // >= 35.6.0
return kuvaLichVersionSixWeapons;
case "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionThree": // >= 35.6.0
case "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionFour": // >= 37.0.0
return corpusVersionThreeWeapons;
}
throw new Error(`unknown nemesis manifest: ${manifest}`);
};
export const isNemesisCompatibleWithVersion = (
nemesis: { manifest: string; Faction: string },
buildLabel: string
): boolean => {
// Anything below 35.6.0 is not going to be okay given our set of supported manifests.
if (version_compare(buildLabel, "2024.05.15.11.07") < 0) {
return false;
}
if (nemesis.Faction == "FC_INFESTATION") {
// Anything below 38.5.0 isn't gonna like an infested lich.
if (version_compare(buildLabel, "2025.03.18.16.07") < 0) {
return false;
}
} else if (nemesis.manifest == "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionFour") {
// Anything below 37.0.0 isn't gonna know version 4, but version 3 is identical in terms of weapon choices, so we can spoof it to that.
if (version_compare(buildLabel, "2024.10.01.11.03") < 0) {
nemesis.manifest = "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionThree";
}
}
return true;
};
export const getInnateDamageTag = (
KillingSuit: string
):
| "InnateElectricityDamage"
| "InnateFreezeDamage"
| "InnateHeatDamage"
| "InnateImpactDamage"
| "InnateMagDamage"
| "InnateRadDamage"
| "InnateToxinDamage" => {
export const getInnateDamageTag = (KillingSuit: string): TInnateDamageTag => {
return ExportWarframes[KillingSuit].nemesisUpgradeTag!;
};
// TODO: For -1399275245665749231n, the value should be 75306944, but we're off by 59 with 75307003.
export const getInnateDamageValue = (fp: bigint): number => {
const petHeads = [
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadA",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadB",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadC"
] as const;
export interface INemesisProfile {
innateDamageTag: TInnateDamageTag;
innateDamageValue: number;
ephemera?: string;
petHead?: (typeof petHeads)[number];
petBody?: string;
petLegs?: string;
petTail?: string;
}
export const generateNemesisProfile = (
fp: bigint = generateRewardSeed(),
manifest: INemesisManifest = new LawyerManifest(),
killingSuit: string = "/Lotus/Powersuits/Ember/Ember"
): INemesisProfile => {
const rng = new SRng(fp);
rng.randomFloat(); // used for the weapon index
const WeaponUpgradeValueAttenuationExponent = 2.25;
@ -292,7 +426,33 @@ export const getInnateDamageValue = (fp: bigint): number => {
if (value >= 0.941428) {
value = 1;
}
return Math.trunc(value * 0x40000000);
const profile: INemesisProfile = {
innateDamageTag: getInnateDamageTag(killingSuit),
innateDamageValue: Math.trunc(value * 0x40000000) // TODO: For -1399275245665749231n, the value should be 75306944, but we're off by 59 with 75307003.
};
if (rng.randomFloat() <= manifest.ephemeraChance && manifest.ephemeraTypes) {
profile.ephemera = manifest.ephemeraTypes[profile.innateDamageTag];
}
rng.randomFloat(); // something related to sentinel agent maybe
if (manifest instanceof LawyerManifest) {
profile.petHead = rng.randomElement(petHeads)!;
profile.petBody = rng.randomElement([
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyA",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyB",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyC"
])!;
profile.petLegs = rng.randomElement([
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsA",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsB",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsC"
])!;
profile.petTail = rng.randomElement([
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailA",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailB",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailC"
])!;
}
return profile;
};
export const getKillTokenRewardCount = (fp: bigint): number => {
@ -351,52 +511,3 @@ export const getInfestedLichItemRewards = (fp: bigint): string[] => {
const rotBReward = getRewardAtPercentage(infestedLichRotB, rng.randomFloat())!.type;
return [rotAReward, rotBReward];
};
export const sendCodaFinishedMessage = async (
inventory: TInventoryDatabaseDocument,
fp: bigint = generateRewardSeed(),
name: string = "ZEKE_BEATWOMAN_TM.1999",
killed: boolean = true
): Promise<void> => {
const att: string[] = [];
// First vanquish/convert gives a sigil
const sigil = killed
? "/Lotus/Upgrades/Skins/Sigils/InfLichVanquishedSigil"
: "/Lotus/Upgrades/Skins/Sigils/InfLichConvertedSigil";
if (!inventory.WeaponSkins.find(x => x.ItemType == sigil)) {
att.push(toStoreItem(sigil));
}
const [rotAReward, rotBReward] = getInfestedLichItemRewards(fp);
att.push(fromStoreItem(rotAReward));
att.push(fromStoreItem(rotBReward));
let countedAtt: ITypeCount[] | undefined;
if (killed) {
countedAtt = [
{
ItemType: "/Lotus/Types/Items/MiscItems/CodaWeaponBucks",
ItemCount: getKillTokenRewardCount(fp)
}
];
}
await createMessage(inventory.accountOwnerId, [
{
sndr: "/Lotus/Language/Bosses/Ordis",
msg: "/Lotus/Language/Inbox/VanquishBandMsgBody",
arg: [
{
Key: "LICH_NAME",
Tag: name
}
],
att: att,
countedAtt: countedAtt,
sub: "/Lotus/Language/Inbox/VanquishBandMsgTitle",
icon: "/Lotus/Interface/Icons/Npcs/Ordis.png",
highPriority: true
}
]);
};

View File

@ -1,4 +1,5 @@
import path from "path";
export const rootDir = path.join(__dirname, "../..");
export const repoDir = path.basename(rootDir) == "build" ? path.join(rootDir, "..") : rootDir;
export const isDev = path.basename(rootDir) != "build";
export const repoDir = isDev ? rootDir : path.join(rootDir, "..");

View File

@ -2,7 +2,7 @@ import { JSONParse } from "json-with-bigint";
export const getJSONfromString = <T>(str: string): T => {
const jsonSubstring = str.substring(0, str.lastIndexOf("}") + 1);
return JSONParse<T>(jsonSubstring);
return JSONParse(jsonSubstring) as T;
};
export const getSubstringFromKeyword = (str: string, keyword: string): string => {

View File

@ -10,3 +10,14 @@ export const getMaxStanding = (syndicate: ISyndicate, title: number): number =>
}
return syndicate.titles.find(x => x.level == title)!.maxStanding;
};
export const getMinStanding = (syndicate: ISyndicate, title: number): number => {
if (!syndicate.titles) {
// LibrarySyndicate
return 0;
}
if (title == 0) {
return syndicate.titles.find(x => x.level == -1)!.maxStanding;
}
return syndicate.titles.find(x => x.level == title)!.minStanding;
};

View File

@ -12,18 +12,14 @@ import { logger } from "@/src/utils/logger";
logger.info("Starting up...");
// Proceed with normal startup: bring up config watcher service, validate config, connect to MongoDB, and finally start listening for HTTP.
import http from "http";
import https from "https";
import fs from "node:fs";
import { app } from "./app";
import mongoose from "mongoose";
import { Json, JSONStringify } from "json-with-bigint";
import { JSONStringify } from "json-with-bigint";
import { startWebServer } from "./services/webService";
import { validateConfig } from "@/src/services/configWatcherService";
// Patch JSON.stringify to work flawlessly with Bigints.
JSON.stringify = (obj: Exclude<Json, undefined>, _replacer?: unknown, space?: string | number): string => {
return JSONStringify(obj, space);
};
JSON.stringify = JSONStringify;
validateConfig();
@ -31,26 +27,7 @@ mongoose
.connect(config.mongodbUrl)
.then(() => {
logger.info("Connected to MongoDB");
const httpPort = config.httpPort || 80;
const httpsPort = config.httpsPort || 443;
const options = {
key: fs.readFileSync("static/certs/key.pem"),
cert: fs.readFileSync("static/certs/cert.pem")
};
// eslint-disable-next-line @typescript-eslint/no-misused-promises
http.createServer(app).listen(httpPort, () => {
logger.info("HTTP server started on port " + httpPort);
// eslint-disable-next-line @typescript-eslint/no-misused-promises
https.createServer(options, app).listen(httpsPort, () => {
logger.info("HTTPS server started on port " + httpsPort);
logger.info(
"Access the WebUI in your browser at http://localhost" + (httpPort == 80 ? "" : ":" + httpPort)
);
});
});
startWebServer();
})
.catch(error => {
if (error instanceof Error) {

View File

@ -1,10 +1,12 @@
import { ISession, IFindSessionRequest } from "@/src/types/session";
import { logger } from "@/src/utils/logger";
import { JSONParse } from "json-with-bigint";
import { Types } from "mongoose";
const sessions: ISession[] = [];
function createNewSession(sessionData: ISession, Creator: string): ISession {
const sessionId = getNewSessionID();
function createNewSession(sessionData: ISession, Creator: Types.ObjectId): ISession {
const sessionId = new Types.ObjectId();
const newSession: ISession = {
sessionId,
creatorId: Creator,
@ -25,7 +27,7 @@ function createNewSession(sessionData: ISession, Creator: string): ISession {
customSettings: sessionData.customSettings || "",
rewardSeed: sessionData.rewardSeed || -1,
guildId: sessionData.guildId || "",
buildId: sessionData.buildId || 4920386201513015989,
buildId: sessionData.buildId || 4920386201513015989n,
platform: sessionData.platform || 0,
xplatform: sessionData.xplatform || true,
freePublic: sessionData.freePublic || 3,
@ -40,13 +42,15 @@ function getAllSessions(): ISession[] {
return sessions;
}
function getSessionByID(sessionId: string): ISession | undefined {
return sessions.find(session => session.sessionId === sessionId);
function getSessionByID(sessionId: string | Types.ObjectId): ISession | undefined {
return sessions.find(session => session.sessionId.equals(sessionId));
}
function getSession(sessionIdOrRequest: string | IFindSessionRequest): { createdBy: string; id: string }[] {
if (typeof sessionIdOrRequest === "string") {
const session = sessions.find(session => session.sessionId === sessionIdOrRequest);
function getSession(
sessionIdOrRequest: string | Types.ObjectId | IFindSessionRequest
): { createdBy: Types.ObjectId; id: Types.ObjectId }[] {
if (typeof sessionIdOrRequest === "string" || sessionIdOrRequest instanceof Types.ObjectId) {
const session = sessions.find(session => session.sessionId.equals(sessionIdOrRequest));
if (session) {
logger.debug("Found Sessions:", { session });
return [
@ -79,35 +83,15 @@ function getSession(sessionIdOrRequest: string | IFindSessionRequest): { created
}));
}
function getSessionByCreatorID(creatorId: string): ISession | undefined {
return sessions.find(session => session.creatorId === creatorId);
function getSessionByCreatorID(creatorId: string | Types.ObjectId): ISession | undefined {
return sessions.find(session => session.creatorId.equals(creatorId));
}
function getNewSessionID(): string {
const characters = "0123456789abcdef";
const maxAttempts = 100;
let sessionId = "";
for (let attempt = 0; attempt < maxAttempts; attempt++) {
sessionId = "64";
for (let i = 0; i < 22; i++) {
const randomIndex = Math.floor(Math.random() * characters.length);
sessionId += characters[randomIndex];
}
if (!sessions.some(session => session.sessionId === sessionId)) {
return sessionId;
}
}
throw new Error("Failed to generate a unique session ID");
}
function updateSession(sessionId: string, sessionData: string): boolean {
const session = sessions.find(session => session.sessionId === sessionId);
function updateSession(sessionId: string | Types.ObjectId, sessionData: string): boolean {
const session = sessions.find(session => session.sessionId.equals(sessionId));
if (!session) return false;
try {
Object.assign(session, JSON.parse(sessionData));
Object.assign(session, JSONParse(sessionData));
return true;
} catch (error) {
console.error("Invalid JSON string for session update.");
@ -115,8 +99,8 @@ function updateSession(sessionId: string, sessionData: string): boolean {
}
}
function deleteSession(sessionId: string): boolean {
const index = sessions.findIndex(session => session.sessionId === sessionId);
function deleteSession(sessionId: string | Types.ObjectId): boolean {
const index = sessions.findIndex(session => session.sessionId.equals(sessionId));
if (index !== -1) {
sessions.splice(index, 1);
return true;
@ -129,7 +113,6 @@ export {
getAllSessions,
getSessionByID,
getSessionByCreatorID,
getNewSessionID,
updateSession,
deleteSession,
getSession

15
src/models/friendModel.ts Normal file
View File

@ -0,0 +1,15 @@
import { IFriendship } from "@/src/types/friendTypes";
import { model, Schema } from "mongoose";
const friendshipSchema = new Schema<IFriendship>({
owner: { type: Schema.Types.ObjectId, required: true },
friend: { type: Schema.Types.ObjectId, required: true },
Note: String,
Favorite: Boolean
});
friendshipSchema.index({ owner: 1 });
friendshipSchema.index({ friend: 1 });
friendshipSchema.index({ owner: 1, friend: 1 }, { unique: true });
export const Friendship = model<IFriendship>("Friendship", friendshipSchema);

View File

@ -13,7 +13,8 @@ import {
IDojoLeaderboardEntry,
IGuildAdDatabase,
IAllianceDatabase,
IAllianceMemberDatabase
IAllianceMemberDatabase,
GuildPermission
} from "@/src/types/guildTypes";
import { Document, Model, model, Schema, Types } from "mongoose";
import { fusionTreasuresSchema, typeCountSchema } from "./inventoryModels/inventoryModel";
@ -108,31 +109,31 @@ const defaultRanks: IGuildRank[] = [
},
{
Name: "/Lotus/Language/Game/Rank_General",
Permissions: 4318
Permissions: GuildPermission.Host | 4318
},
{
Name: "/Lotus/Language/Game/Rank_Officer",
Permissions: 4314
Permissions: GuildPermission.Host | 4314
},
{
Name: "/Lotus/Language/Game/Rank_Leader",
Permissions: 4106
Permissions: GuildPermission.Host | 4106
},
{
Name: "/Lotus/Language/Game/Rank_Sage",
Permissions: 4304
Permissions: GuildPermission.Host | 4304
},
{
Name: "/Lotus/Language/Game/Rank_Soldier",
Permissions: 4098
Permissions: GuildPermission.Host | 4098
},
{
Name: "/Lotus/Language/Game/Rank_Initiate",
Permissions: 4096
Permissions: GuildPermission.Host | GuildPermission.Fabricator
},
{
Name: "/Lotus/Language/Game/Rank_Utility",
Permissions: 4096
Permissions: GuildPermission.Host | GuildPermission.Fabricator
}
];

View File

@ -38,7 +38,8 @@ import {
IPeriodicMissionCompletionResponse,
ILoreFragmentScan,
IEvolutionProgress,
IEndlessXpProgress,
IEndlessXpProgressDatabase,
IEndlessXpProgressClient,
ICrewShipCustomization,
ICrewShipWeapon,
ICrewShipWeaponEmplacements,
@ -97,7 +98,8 @@ import {
IInvasionProgressClient,
IAccolades,
IHubNpcCustomization,
ILotusCustomization
ILotusCustomization,
IEndlessXpReward
} from "../../types/inventoryTypes/inventoryTypes";
import { IOid } from "../../types/commonTypes";
import {
@ -112,6 +114,7 @@ import {
} from "@/src/types/inventoryTypes/commonInventoryTypes";
import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers";
import { EquipmentSelectionSchema, oidSchema } from "./loadoutModel";
import { ICountedStoreItem } from "warframe-public-export-plus";
export const typeCountSchema = new Schema<ITypeCount>({ ItemType: String, ItemCount: Number }, { _id: false });
@ -810,14 +813,48 @@ const evolutionProgressSchema = new Schema<IEvolutionProgress>(
{ _id: false }
);
const endlessXpProgressSchema = new Schema<IEndlessXpProgress>(
const countedStoreItemSchema = new Schema<ICountedStoreItem>(
{
Category: String,
Choices: [String]
StoreItem: String,
ItemCount: Number
},
{ _id: false }
);
const endlessXpRewardSchema = new Schema<IEndlessXpReward>(
{
RequiredTotalXp: Number,
Rewards: [countedStoreItemSchema]
},
{ _id: false }
);
const endlessXpProgressSchema = new Schema<IEndlessXpProgressDatabase>(
{
Category: { type: String, required: true },
Earn: { type: Number, default: 0 },
Claim: { type: Number, default: 0 },
BonusAvailable: Date,
Expiry: Date,
Choices: { type: [String], required: true },
PendingRewards: { type: [endlessXpRewardSchema], default: [] }
},
{ _id: false }
);
endlessXpProgressSchema.set("toJSON", {
transform(_doc, ret) {
const db = ret as IEndlessXpProgressDatabase;
const client = ret as IEndlessXpProgressClient;
if (db.BonusAvailable) {
client.BonusAvailable = toMongoDate(db.BonusAvailable);
}
if (db.Expiry) {
client.Expiry = toMongoDate(db.Expiry);
}
}
});
const crewShipWeaponEmplacementsSchema = new Schema<ICrewShipWeaponEmplacements>(
{
PRIMARY_A: EquipmentSelectionSchema,
@ -1060,7 +1097,8 @@ const pendingRecipeSchema = new Schema<IPendingRecipeDatabase>(
LongGuns: { type: [EquipmentSchema], default: undefined },
Pistols: { type: [EquipmentSchema], default: undefined },
Melee: { type: [EquipmentSchema], default: undefined },
SuitToUnbrand: { type: Schema.Types.ObjectId, default: undefined }
SuitToUnbrand: { type: Schema.Types.ObjectId, default: undefined },
KubrowPet: { type: Schema.Types.ObjectId, default: undefined }
},
{ id: false }
);
@ -1078,6 +1116,7 @@ pendingRecipeSchema.set("toJSON", {
delete returnedObject.Pistols;
delete returnedObject.Melees;
delete returnedObject.SuitToUnbrand;
delete returnedObject.KubrowPet;
(returnedObject as IPendingRecipeClient).CompletionDate = {
$date: { $numberLong: (returnedObject as IPendingRecipeDatabase).CompletionDate.getTime().toString() }
};
@ -1281,7 +1320,7 @@ const nemesisSchema = new Schema<INemesisDatabase>(
InfNodes: { type: [infNodeSchema], default: undefined },
HenchmenKilled: Number,
HintProgress: Number,
Hints: { type: [Number], default: undefined },
Hints: { type: [Number], default: [] },
GuessHistory: { type: [Number], default: undefined },
MissionCount: Number,
LastEnc: Number
@ -1584,7 +1623,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
Drones: [droneSchema],
//Active profile ico
ActiveAvatarImageType: { type: String, default: "/Lotus/Types/StoreItems/AvatarImages/AvatarImageDefault" },
ActiveAvatarImageType: String,
// open location store like EidolonPlainsDiscoverable or OrbVallisCaveDiscoverable
DiscoveredMarkers: [discoveredMarkerSchema],

View File

@ -22,6 +22,7 @@ const databaseAccountSchema = new Schema<IDatabaseAccountJson>(
Nonce: { type: Number, default: 0 },
BuildLabel: String,
Dropped: Boolean,
LastLogin: { type: Date, default: 0 },
LatestEventMessageDate: { type: Date, default: 0 },
LastLoginRewardDate: { type: Number, default: 0 },
LoginDays: { type: Number, default: 1 }

View File

@ -13,7 +13,7 @@ import {
IPlantDatabase,
IPlantClient
} from "@/src/types/shipTypes";
import { Schema, model } from "mongoose";
import { Schema, Types, model } from "mongoose";
export const pictureFrameInfoSchema = new Schema<IPictureFrameInfo>(
{
@ -153,7 +153,18 @@ const orbiterDefault: IOrbiter = {
Features: ["/Lotus/Types/Items/ShipFeatureItems/EarthNavigationFeatureItem"], //TODO: potentially remove after missionstarting gear
Rooms: [
{ Name: "AlchemyRoom", MaxCapacity: 1600 },
{ Name: "BridgeRoom", MaxCapacity: 1600 },
{
Name: "BridgeRoom",
MaxCapacity: 1600,
PlacedDecos: [
{
Type: "/Lotus/Objects/Tenno/Props/Ships/LandCraftPlayerProps/ConclaveConsolePlayerShipDeco",
Pos: [-30.082, -3.95954, -16.7913],
Rot: [-135, 0, 0],
_id: undefined as unknown as Types.ObjectId
}
]
},
{ Name: "LisetRoom", MaxCapacity: 1000 },
{ Name: "OperatorChamberRoom", MaxCapacity: 1600 },
{ Name: "OutsideRoom", MaxCapacity: 1600 },

View File

@ -28,17 +28,15 @@ shipSchema.set("toJSON", {
delete returnedObject._id;
delete returnedObject.__v;
delete returnedObject.ShipOwnerId;
if (shipDatabase.ShipExteriorColors) {
shipResponse.ShipExterior = {
Colors: shipDatabase.ShipExteriorColors,
ShipAttachments: shipDatabase.ShipAttachments,
SkinFlavourItem: shipDatabase.SkinFlavourItem
};
delete shipDatabase.ShipExteriorColors;
delete shipDatabase.ShipAttachments;
delete shipDatabase.SkinFlavourItem;
}
shipResponse.ShipExterior = {
Colors: shipDatabase.ShipExteriorColors,
ShipAttachments: shipDatabase.ShipAttachments,
SkinFlavourItem: shipDatabase.SkinFlavourItem
};
delete shipDatabase.ShipExteriorColors;
delete shipDatabase.ShipAttachments;
delete shipDatabase.SkinFlavourItem;
}
});

View File

@ -3,10 +3,13 @@ import { abandonLibraryDailyTaskController } from "@/src/controllers/api/abandon
import { abortDojoComponentController } from "@/src/controllers/api/abortDojoComponentController";
import { abortDojoComponentDestructionController } from "@/src/controllers/api/abortDojoComponentDestructionController";
import { activateRandomModController } from "@/src/controllers/api/activateRandomModController";
import { addFriendController } from "@/src/controllers/api/addFriendController";
import { addFriendImageController } from "@/src/controllers/api/addFriendImageController";
import { addIgnoredUserController } from "@/src/controllers/api/addIgnoredUserController";
import { addPendingFriendController } from "@/src/controllers/api/addPendingFriendController";
import { addToAllianceController } from "@/src/controllers/api/addToAllianceController";
import { addToGuildController } from "@/src/controllers/api/addToGuildController";
import { adoptPetController } from "@/src/controllers/api/adoptPetController";
import { arcaneCommonController } from "@/src/controllers/api/arcaneCommonController";
import { archonFusionController } from "@/src/controllers/api/archonFusionController";
import { artifactsController } from "@/src/controllers/api/artifactsController";
@ -97,12 +100,15 @@ import { playerSkillsController } from "@/src/controllers/api/playerSkillsContro
import { postGuildAdvertisementController } from "@/src/controllers/api/postGuildAdvertisementController";
import { projectionManagerController } from "@/src/controllers/api/projectionManagerController";
import { purchaseController } from "@/src/controllers/api/purchaseController";
import { questControlController } from "@/src/controllers/api/questControlController";
import { queueDojoComponentDestructionController } from "@/src/controllers/api/queueDojoComponentDestructionController";
import { redeemPromoCodeController } from "@/src/controllers/api/redeemPromoCodeController";
import { releasePetController } from "@/src/controllers/api/releasePetController";
import { removeFriendGetController, removeFriendPostController } from "@/src/controllers/api/removeFriendController";
import { removeFromAllianceController } from "@/src/controllers/api/removeFromAllianceController";
import { removeFromGuildController } from "@/src/controllers/api/removeFromGuildController";
import { removeIgnoredUserController } from "@/src/controllers/api/removeIgnoredUserController";
import { renamePetController } from "@/src/controllers/api/renamePetController";
import { rerollRandomModController } from "@/src/controllers/api/rerollRandomModController";
import { retrievePetFromStasisController } from "@/src/controllers/api/retrievePetFromStasisController";
import { saveDialogueController } from "@/src/controllers/api/saveDialogueController";
@ -119,6 +125,7 @@ import { setDojoComponentColorsController } from "@/src/controllers/api/setDojoC
import { setDojoComponentMessageController } from "@/src/controllers/api/setDojoComponentMessageController";
import { setDojoComponentSettingsController } from "@/src/controllers/api/setDojoComponentSettingsController";
import { setEquippedInstrumentController } from "@/src/controllers/api/setEquippedInstrumentController";
import { setFriendNoteController } from "@/src/controllers/api/setFriendNoteController";
import { setGuildMotdController } from "@/src/controllers/api/setGuildMotdController";
import { setHubNpcCustomizationsController } from "@/src/controllers/api/setHubNpcCustomizationsController";
import { setPlacedDecoInfoController } from "@/src/controllers/api/setPlacedDecoInfoController";
@ -179,8 +186,10 @@ apiRouter.get("/getGuildContributions.php", getGuildContributionsController);
apiRouter.get("/getGuildDojo.php", getGuildDojoController);
apiRouter.get("/getGuildLog.php", getGuildLogController);
apiRouter.get("/getIgnoredUsers.php", getIgnoredUsersController);
apiRouter.get("/getMessages.php", inboxController); // unsure if this is correct, but needed for U17
apiRouter.get("/getNewRewardSeed.php", getNewRewardSeedController);
apiRouter.get("/getShip.php", getShipController);
apiRouter.get("/getShipDecos.php", (_req, res) => { res.end(); }); // needed to log in on U22.8
apiRouter.get("/getVendorInfo.php", getVendorInfoController);
apiRouter.get("/hub", hubController);
apiRouter.get("/hubInstances", hubInstancesController);
@ -192,7 +201,9 @@ apiRouter.get("/marketRecommendations.php", marketRecommendationsController);
apiRouter.get("/marketSearchRecommendations.php", marketRecommendationsController);
apiRouter.get("/modularWeaponSale.php", modularWeaponSaleController);
apiRouter.get("/playedParkourTutorial.php", playedParkourTutorialController);
apiRouter.get("/questControl.php", questControlController);
apiRouter.get("/queueDojoComponentDestruction.php", queueDojoComponentDestructionController);
apiRouter.get("/removeFriend.php", removeFriendGetController);
apiRouter.get("/removeFromAlliance.php", removeFromAllianceController);
apiRouter.get("/setActiveQuest.php", setActiveQuestController);
apiRouter.get("/setActiveShip.php", setActiveShipController);
@ -210,10 +221,13 @@ apiRouter.get("/updateSession.php", updateSessionGetController);
// post
apiRouter.post("/abortDojoComponent.php", abortDojoComponentController);
apiRouter.post("/activateRandomMod.php", activateRandomModController);
apiRouter.post("/addFriend.php", addFriendController);
apiRouter.post("/addFriendImage.php", addFriendImageController);
apiRouter.post("/addIgnoredUser.php", addIgnoredUserController);
apiRouter.post("/addPendingFriend.php", addPendingFriendController);
apiRouter.post("/addToAlliance.php", addToAllianceController);
apiRouter.post("/addToGuild.php", addToGuildController);
apiRouter.post("/adoptPet.php", adoptPetController);
apiRouter.post("/arcaneCommon.php", arcaneCommonController);
apiRouter.post("/archonFusion.php", archonFusionController);
apiRouter.post("/artifacts.php", artifactsController);
@ -222,6 +236,7 @@ apiRouter.post("/changeDojoRoot.php", changeDojoRootController);
apiRouter.post("/claimCompletedRecipe.php", claimCompletedRecipeController);
apiRouter.post("/clearDialogueHistory.php", clearDialogueHistoryController);
apiRouter.post("/clearNewEpisodeReward.php", clearNewEpisodeRewardController);
apiRouter.post("/commitStoryModeDecision.php", (_req, res) => { res.end(); }); // U14 (maybe wanna actually unlock the ship features?)
apiRouter.post("/completeRandomModChallenge.php", completeRandomModChallengeController);
apiRouter.post("/confirmGuildInvitation.php", confirmGuildInvitationPostController);
apiRouter.post("/contributeGuildClass.php", contributeGuildClassController);
@ -276,10 +291,13 @@ apiRouter.post("/playerSkills.php", playerSkillsController);
apiRouter.post("/postGuildAdvertisement.php", postGuildAdvertisementController);
apiRouter.post("/projectionManager.php", projectionManagerController);
apiRouter.post("/purchase.php", purchaseController);
apiRouter.post("/questControl.php", questControlController); // U17
apiRouter.post("/redeemPromoCode.php", redeemPromoCodeController);
apiRouter.post("/releasePet.php", releasePetController);
apiRouter.post("/removeFriend.php", removeFriendPostController);
apiRouter.post("/removeFromGuild.php", removeFromGuildController);
apiRouter.post("/removeIgnoredUser.php", removeIgnoredUserController);
apiRouter.post("/renamePet.php", renamePetController);
apiRouter.post("/rerollRandomMod.php", rerollRandomModController);
apiRouter.post("/retrievePetFromStasis.php", retrievePetFromStasisController);
apiRouter.post("/saveDialogue.php", saveDialogueController);
@ -292,6 +310,7 @@ apiRouter.post("/setDojoComponentColors.php", setDojoComponentColorsController);
apiRouter.post("/setDojoComponentMessage.php", setDojoComponentMessageController);
apiRouter.post("/setDojoComponentSettings.php", setDojoComponentSettingsController);
apiRouter.post("/setEquippedInstrument.php", setEquippedInstrumentController);
apiRouter.post("/setFriendNote.php", setFriendNoteController);
apiRouter.post("/setGuildMotd.php", setGuildMotdController);
apiRouter.post("/setHubNpcCustomizations.php", setHubNpcCustomizationsController);
apiRouter.post("/setPlacedDecoInfo.php", setPlacedDecoInfoController);

View File

@ -23,6 +23,7 @@ import { setEvolutionProgressController } from "@/src/controllers/custom/setEvol
import { getConfigDataController } from "@/src/controllers/custom/getConfigDataController";
import { updateConfigDataController } from "@/src/controllers/custom/updateConfigDataController";
import { setBoosterController } from "../controllers/custom/setBoosterController";
const customRouter = express.Router();
@ -46,6 +47,7 @@ customRouter.post("/addXp", addXpController);
customRouter.post("/import", importController);
customRouter.post("/manageQuests", manageQuestsController);
customRouter.post("/setEvolutionProgress", setEvolutionProgressController);
customRouter.post("/setBooster", setBoosterController);
customRouter.get("/config", getConfigDataController);
customRouter.post("/config", updateConfigDataController);

View File

@ -24,6 +24,8 @@ interface IConfig {
infiniteEndo?: boolean;
infiniteRegalAya?: boolean;
infiniteHelminthMaterials?: boolean;
claimingBlueprintRefundsIngredients?: boolean;
dontSubtractVoidTraces?: boolean;
dontSubtractConsumables?: boolean;
unlockAllShipFeatures?: boolean;
unlockAllShipDecorations?: boolean;
@ -42,6 +44,8 @@ interface IConfig {
noVendorPurchaseLimits?: boolean;
noDeathMarks?: boolean;
noKimCooldowns?: boolean;
syndicateMissionsRepeatable?: boolean;
instantFinishRivenChallenge?: boolean;
instantResourceExtractorDrones?: boolean;
noResourceExtractorDronesDamage?: boolean;
skipClanKeyCrafting?: boolean;
@ -52,12 +56,18 @@ interface IConfig {
noDojoResearchTime?: boolean;
fastClanAscension?: boolean;
spoofMasteryRank?: number;
nightwaveStandingMultiplier?: number;
worldState?: {
creditBoost?: boolean;
affinityBoost?: boolean;
resourceBoost?: boolean;
starDays?: boolean;
lockTime?: number;
eidolonOverride?: string;
vallisOverride?: string;
nightwaveOverride?: string;
};
dev?: {
keepVendorsExpired?: boolean;
};
}

View File

@ -2,6 +2,7 @@ import fs from "fs";
import fsPromises from "fs/promises";
import { logger } from "../utils/logger";
import { config, configPath, loadConfig } from "./configService";
import { getWebPorts, startWebServer, stopWebServer } from "./webService";
let amnesia = false;
fs.watchFile(configPath, () => {
@ -16,13 +17,31 @@ fs.watchFile(configPath, () => {
process.exit(1);
}
validateConfig();
const webPorts = getWebPorts();
if (config.httpPort != webPorts.http || config.httpsPort != webPorts.https) {
logger.info(`Restarting web server to apply port changes.`);
void stopWebServer().then(startWebServer);
}
}
});
export const validateConfig = (): void => {
if (typeof config.administratorNames == "string") {
logger.info(`Updating config.json to make administratorNames an array.`);
config.administratorNames = [config.administratorNames];
let modified = false;
if (config.administratorNames) {
if (!Array.isArray(config.administratorNames)) {
config.administratorNames = [config.administratorNames];
modified = true;
}
for (let i = 0; i != config.administratorNames.length; ++i) {
if (typeof config.administratorNames[i] != "string") {
config.administratorNames[i] = String(config.administratorNames[i]);
modified = true;
}
}
}
if (modified) {
logger.info(`Updating config.json to fix some issues with it.`);
void saveConfig();
}
};

View File

@ -0,0 +1,47 @@
import { IFriendInfo } from "../types/friendTypes";
import { getInventory } from "./inventoryService";
import { config } from "./configService";
import { Account } from "../models/loginModel";
import { Types } from "mongoose";
import { Friendship } from "../models/friendModel";
import { fromOid, toMongoDate } from "../helpers/inventoryHelpers";
export const addAccountDataToFriendInfo = async (info: IFriendInfo): Promise<void> => {
const account = (await Account.findById(fromOid(info._id), "DisplayName LastLogin"))!;
info.DisplayName = account.DisplayName;
info.LastLogin = toMongoDate(account.LastLogin);
};
export const addInventoryDataToFriendInfo = async (info: IFriendInfo): Promise<void> => {
const inventory = await getInventory(fromOid(info._id), "PlayerLevel ActiveAvatarImageType");
info.PlayerLevel = config.spoofMasteryRank == -1 ? inventory.PlayerLevel : config.spoofMasteryRank;
info.ActiveAvatarImageType = inventory.ActiveAvatarImageType;
};
export const areFriends = async (a: Types.ObjectId | string, b: Types.ObjectId | string): Promise<boolean> => {
const [aAddedB, bAddedA] = await Promise.all([
Friendship.exists({ owner: a, friend: b }),
Friendship.exists({ owner: b, friend: a })
]);
return Boolean(aAddedB && bAddedA);
};
export const areFriendsOfFriends = async (a: Types.ObjectId | string, b: Types.ObjectId | string): Promise<boolean> => {
const [aInternalFriends, bInternalFriends] = await Promise.all([
Friendship.find({ owner: a }),
Friendship.find({ owner: b })
]);
for (const aInternalFriend of aInternalFriends) {
if (bInternalFriends.find(x => x.friend.equals(aInternalFriend.friend))) {
const c = aInternalFriend.friend;
const [cAcceptedA, cAcceptedB] = await Promise.all([
Friendship.exists({ owner: c, friend: a }),
Friendship.exists({ owner: c, friend: b })
]);
if (cAcceptedA && cAcceptedB) {
return true;
}
}
}
return false;
};

View File

@ -1,5 +1,5 @@
import { Request } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getAccountIdForRequest, TAccountDocument } from "@/src/services/loginService";
import { addLevelKeys, addRecipes, combineInventoryChanges, getInventory } from "@/src/services/inventoryService";
import { Alliance, AllianceMember, Guild, GuildAd, GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
@ -19,12 +19,11 @@ import {
IGuildVault,
ITechProjectDatabase
} from "@/src/types/guildTypes";
import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers";
import { toMongoDate, toOid, toOid2 } from "@/src/helpers/inventoryHelpers";
import { Types } from "mongoose";
import { ExportDojoRecipes, ExportResources, IDojoBuild, IDojoResearch } from "warframe-public-export-plus";
import { logger } from "../utils/logger";
import { config } from "./configService";
import { Account } from "../models/loginModel";
import { getRandomInt } from "./rngService";
import { Inbox } from "../models/inboxModel";
import { IFusionTreasure, ITypeCount } from "../types/inventoryTypes/inventoryTypes";
@ -32,6 +31,7 @@ import { IInventoryChanges } from "../types/purchaseTypes";
import { parallelForeach } from "../utils/async-utils";
import allDecoRecipes from "@/static/fixed_responses/allDecoRecipes.json";
import { createMessage } from "./inboxService";
import { addAccountDataToFriendInfo, addInventoryDataToFriendInfo } from "./friendService";
export const getGuildForRequest = async (req: Request): Promise<TGuildDatabaseDocument> => {
const accountId = await getAccountIdForRequest(req);
@ -54,7 +54,10 @@ export const getGuildForRequestEx = async (
return guild;
};
export const getGuildClient = async (guild: TGuildDatabaseDocument, accountId: string): Promise<IGuildClient> => {
export const getGuildClient = async (
guild: TGuildDatabaseDocument,
account: TAccountDocument
): Promise<IGuildClient> => {
const guildMembers = await GuildMember.find({ guildId: guild._id });
const members: IGuildMemberClient[] = [];
@ -62,34 +65,30 @@ export const getGuildClient = async (guild: TGuildDatabaseDocument, accountId: s
const dataFillInPromises: Promise<void>[] = [];
for (const guildMember of guildMembers) {
const member: IGuildMemberClient = {
_id: toOid(guildMember.accountId),
_id: toOid2(guildMember.accountId, account.BuildLabel),
Rank: guildMember.rank,
Status: guildMember.status,
Note: guildMember.RequestMsg,
RequestExpiry: guildMember.RequestExpiry ? toMongoDate(guildMember.RequestExpiry) : undefined
};
if (guildMember.accountId.equals(accountId)) {
if (guildMember.accountId.equals(account._id)) {
missingEntry = false;
} else {
dataFillInPromises.push(
(async (): Promise<void> => {
member.DisplayName = (await Account.findById(guildMember.accountId, "DisplayName"))!.DisplayName;
})()
);
dataFillInPromises.push(fillInInventoryDataForGuildMember(member));
dataFillInPromises.push(addAccountDataToFriendInfo(member));
dataFillInPromises.push(addInventoryDataToFriendInfo(member));
}
members.push(member);
}
if (missingEntry) {
// Handle clans created prior to creation of the GuildMember model.
await GuildMember.insertOne({
accountId: accountId,
accountId: account._id,
guildId: guild._id,
status: 0,
rank: 0
});
members.push({
_id: { $oid: accountId },
_id: toOid2(account._id, account.BuildLabel),
Status: 0,
Rank: 0
});
@ -98,7 +97,7 @@ export const getGuildClient = async (guild: TGuildDatabaseDocument, accountId: s
await Promise.all(dataFillInPromises);
return {
_id: toOid(guild._id),
_id: toOid2(guild._id, account.BuildLabel),
Name: guild.Name,
MOTD: guild.MOTD,
LongMOTD: guild.LongMOTD,
@ -110,11 +109,11 @@ export const getGuildClient = async (guild: TGuildDatabaseDocument, accountId: s
ActiveDojoColorResearch: guild.ActiveDojoColorResearch,
Class: guild.Class,
XP: guild.XP,
IsContributor: !!guild.CeremonyContributors?.find(x => x.equals(accountId)),
IsContributor: !!guild.CeremonyContributors?.find(x => x.equals(account._id)),
NumContributors: guild.CeremonyContributors?.length ?? 0,
CeremonyResetDate: guild.CeremonyResetDate ? toMongoDate(guild.CeremonyResetDate) : undefined,
AutoContributeFromVault: guild.AutoContributeFromVault,
AllianceId: guild.AllianceId ? toOid(guild.AllianceId) : undefined
AllianceId: guild.AllianceId ? toOid2(guild.AllianceId, account.BuildLabel) : undefined
};
};
@ -134,10 +133,11 @@ export const getGuildVault = (guild: TGuildDatabaseDocument): IGuildVault => {
export const getDojoClient = async (
guild: TGuildDatabaseDocument,
status: number,
componentId?: Types.ObjectId | string
componentId?: Types.ObjectId | string,
buildLabel?: string
): Promise<IDojoClient> => {
const dojo: IDojoClient = {
_id: { $oid: guild._id.toString() },
_id: toOid2(guild._id, buildLabel),
Name: guild.Name,
Tier: guild.Tier,
GuildEmblem: guild.Emblem,
@ -159,8 +159,8 @@ export const getDojoClient = async (
for (const dojoComponent of guild.DojoComponents) {
if (!componentId || dojoComponent._id.equals(componentId)) {
const clientComponent: IDojoComponentClient = {
id: toOid(dojoComponent._id),
SortId: toOid(dojoComponent.SortId ?? dojoComponent._id), // always providing a SortId so decos don't need repositioning to reparent
id: toOid2(dojoComponent._id, buildLabel),
SortId: toOid2(dojoComponent.SortId ?? dojoComponent._id, buildLabel), // always providing a SortId so decos don't need repositioning to reparent
pf: dojoComponent.pf,
ppf: dojoComponent.ppf,
Name: dojoComponent.Name,
@ -169,7 +169,7 @@ export const getDojoClient = async (
Settings: dojoComponent.Settings
};
if (dojoComponent.pi) {
clientComponent.pi = toOid(dojoComponent.pi);
clientComponent.pi = toOid2(dojoComponent.pi, buildLabel);
clientComponent.op = dojoComponent.op!;
clientComponent.pp = dojoComponent.pp!;
}
@ -225,7 +225,7 @@ export const getDojoClient = async (
clientComponent.Decos = [];
for (const deco of dojoComponent.Decos) {
const clientDeco: IDojoDecoClient = {
id: toOid(deco._id),
id: toOid2(deco._id, buildLabel),
Type: deco.Type,
Pos: deco.Pos,
Rot: deco.Rot,
@ -449,7 +449,7 @@ export const addGuildMemberShipDecoContribution = (guildMember: IGuildMemberData
export const processDojoBuildMaterialsGathered = (guild: TGuildDatabaseDocument, build: IDojoBuild): void => {
if (build.guildXpValue) {
guild.ClaimedXP ??= [];
if (!guild.ClaimedXP.find(x => x == build.resultType)) {
if (guild.ClaimedXP.indexOf(build.resultType) == -1) {
guild.ClaimedXP.push(build.resultType);
guild.XP += build.guildXpValue;
}
@ -466,12 +466,6 @@ export const setDojoRoomLogFunded = (guild: TGuildDatabaseDocument, component: I
}
};
export const fillInInventoryDataForGuildMember = async (member: IGuildMemberClient): Promise<void> => {
const inventory = await getInventory(member._id.$oid, "PlayerLevel ActiveAvatarImageType");
member.PlayerLevel = config.spoofMasteryRank == -1 ? inventory.PlayerLevel : config.spoofMasteryRank;
member.ActiveAvatarImageType = inventory.ActiveAvatarImageType;
};
export const createUniqueClanName = async (name: string): Promise<string> => {
const initialDiscriminator = getRandomInt(0, 999);
let discriminator = initialDiscriminator;

View File

@ -377,9 +377,6 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial<
db[key] = client[key];
}
}
if (client.EndlessXP !== undefined) {
db.EndlessXP = client.EndlessXP;
}
if (client.SongChallenges !== undefined) {
db.SongChallenges = client.SongChallenges;
}

View File

@ -28,7 +28,8 @@ import {
ITraits,
ICalendarProgress,
INemesisWeaponTargetFingerprint,
INemesisPetTargetFingerprint
INemesisPetTargetFingerprint,
IDialogueDatabase
} from "@/src/types/inventoryTypes/inventoryTypes";
import { IGenericUpdate, IUpdateNodeIntrosResponse } from "../types/genericUpdate";
import { IKeyChainRequest, IMissionInventoryUpdateRequest } from "../types/requestTypes";
@ -43,6 +44,7 @@ import {
import {
ExportArcanes,
ExportBundles,
ExportChallenges,
ExportCustoms,
ExportDrones,
ExportEmailItems,
@ -52,7 +54,6 @@ import {
ExportGear,
ExportKeys,
ExportMisc,
ExportNightwave,
ExportRailjackWeapons,
ExportRecipes,
ExportResources,
@ -70,6 +71,7 @@ import { createShip } from "./shipService";
import {
catbrowDetails,
fromMongoDate,
fromOid,
kubrowDetails,
kubrowFurPatternsWeights,
kubrowWeights,
@ -80,9 +82,11 @@ import { handleBundleAcqusition } from "./purchaseService";
import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json";
import { getRandomElement, getRandomInt, getRandomWeightedReward, SRng } from "./rngService";
import { createMessage } from "./inboxService";
import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper";
import { getWorldState } from "./worldStateService";
import { getInnateDamageTag, getInnateDamageValue } from "../helpers/nemesisHelpers";
import { getMaxStanding, getMinStanding } from "@/src/helpers/syndicateStandingHelper";
import { getNightwaveSyndicateTag, getWorldState } from "./worldStateService";
import { generateNemesisProfile, INemesisProfile } from "../helpers/nemesisHelpers";
import { TAccountDocument } from "./loginService";
import { unixTimesInMs } from "../constants/timeConstants";
export const createInventory = async (
accountOwnerId: Types.ObjectId,
@ -149,6 +153,11 @@ export const addStartingGear = async (
inventory: TInventoryDatabaseDocument,
startingGear?: TPartialStartingGear
): Promise<IInventoryChanges> => {
if (inventory.ReceivedStartingGear) {
throw new Error(`account has already received starting gear`);
}
inventory.ReceivedStartingGear = true;
const { LongGuns, Pistols, Suits, Melee } = startingGear || {
LongGuns: [{ ItemType: "/Lotus/Weapons/Tenno/Rifle/Rifle" }],
Pistols: [{ ItemType: "/Lotus/Weapons/Tenno/Pistol/Pistol" }],
@ -197,11 +206,6 @@ export const addStartingGear = async (
combineInventoryChanges(inventoryChanges, inventoryDelta);
}
if (inventory.ReceivedStartingGear) {
logger.warn(`account already had starting gear but asked for it again?!`);
}
inventory.ReceivedStartingGear = true;
return inventoryChanges;
};
@ -423,7 +427,7 @@ export const addItem = async (
changes.push({
ItemType: egg.ItemType,
ExpirationDate: { $date: { $numberLong: "2000000000000" } },
ItemId: toOid(egg._id)
ItemId: toOid(egg._id) // TODO: Pass on buildLabel from purchaseService
});
}
return {
@ -719,6 +723,10 @@ export const addItem = async (
}
break;
case "Boons":
// Can purchase /Lotus/Upgrades/Boons/DuviriVendorBoonItem from Acrithis, doesn't need to be added to inventory.
return {};
case "Stickers":
{
const entry = inventory.RawUpgrades.find(x => x.ItemType == typeName);
@ -773,7 +781,9 @@ export const addItem = async (
typeName.substr(1).split("/")[3] == "CatbrowPet" ||
typeName.substr(1).split("/")[3] == "KubrowPet"
) {
return addKubrowPet(inventory, typeName, undefined, premiumPurchase);
if (typeName != "/Lotus/Types/Game/KubrowPet/Eggs/KubrowPetEggItem") {
return addKubrowPet(inventory, typeName, undefined, premiumPurchase);
}
} else if (typeName.startsWith("/Lotus/Types/Game/CrewShip/CrewMember/")) {
if (!seed) {
throw new Error(`Expected crew member to have a seed`);
@ -788,6 +798,12 @@ export const addItem = async (
}
break;
}
case "Items": {
if (typeName.substr(1).split("/")[3] == "Emotes") {
return addCustomization(inventory, typeName);
}
break;
}
case "NeutralCreatures": {
if (inventory.Horses.length != 0) {
logger.warn("refusing to add Horse because account already has one");
@ -869,10 +885,14 @@ const addSentinel = (
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const configs: IItemConfig[] = applyDefaultUpgrades(inventory, ExportSentinels[sentinelName]?.defaultUpgrades);
const features = premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined;
const sentinelIndex =
inventory.Sentinels.push({ ItemType: sentinelName, Configs: configs, XP: 0, Features: features, IsNew: true }) -
1;
inventory.Sentinels.push({
ItemType: sentinelName,
Configs: configs,
XP: 0,
Features: premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined,
IsNew: inventory.Sentinels.find(x => x.ItemType == sentinelName) ? undefined : true
}) - 1;
inventoryChanges.Sentinels ??= [];
inventoryChanges.Sentinels.push(inventory.Sentinels[sentinelIndex].toJSON<IEquipmentClient>());
@ -921,6 +941,9 @@ export const addPowerSuit = async (
},
defaultOverwrites
);
if (suit.IsNew) {
suit.IsNew = !inventory.Suits.find(x => x.ItemType == powersuitName);
}
if (!suit.IsNew) {
suit.IsNew = undefined;
}
@ -955,7 +978,7 @@ export const addMechSuit = async (
UpgradeVer: 101,
XP: 0,
Features: features,
IsNew: true
IsNew: inventory.MechSuits.find(x => x.ItemType == mechsuitName) ? undefined : true
}) - 1;
inventoryChanges.MechSuits ??= [];
inventoryChanges.MechSuits.push(inventory.MechSuits[suitIndex].toJSON<IEquipmentClient>());
@ -995,7 +1018,7 @@ export const addSpaceSuit = (
UpgradeVer: 101,
XP: 0,
Features: features,
IsNew: true
IsNew: inventory.SpaceSuits.find(x => x.ItemType == spacesuitName) ? undefined : true
}) - 1;
inventoryChanges.SpaceSuits ??= [];
inventoryChanges.SpaceSuits.push(inventory.SpaceSuits[suitIndex].toJSON<IEquipmentClient>());
@ -1005,12 +1028,13 @@ export const addSpaceSuit = (
export const addKubrowPet = (
inventory: TInventoryDatabaseDocument,
kubrowPetName: string,
details: IKubrowPetDetailsDatabase | undefined,
premiumPurchase: boolean,
details?: IKubrowPetDetailsDatabase,
premiumPurchase: boolean = false,
inventoryChanges: IInventoryChanges = {}
): IInventoryChanges => {
combineInventoryChanges(inventoryChanges, occupySlot(inventory, InventorySlot.SENTINELS, premiumPurchase));
// TODO: When incubating, this should only be given when claiming the recipe.
const kubrowPet = ExportSentinels[kubrowPetName] as ISentinel | undefined;
const exalted = kubrowPet?.exalted ?? [];
for (const specialItem of exalted) {
@ -1059,11 +1083,11 @@ export const addKubrowPet = (
details = {
Name: "",
IsPuppy: false,
IsPuppy: !premiumPurchase,
HasCollar: true,
PrintsRemaining: 2,
Status: Status.StatusStasis,
HatchDate: new Date(Math.trunc(Date.now() / 86400000) * 86400000),
PrintsRemaining: 3,
Status: premiumPurchase ? Status.StatusStasis : Status.StatusIncubating,
HatchDate: premiumPurchase ? new Date() : new Date(Date.now() + 10 * unixTimesInMs.hour), // On live, this seems to be somewhat randomised so that the pet hatches 9~11 hours after start.
IsMale: !!getRandomInt(0, 1),
Size: getRandomInt(70, 100) / 100,
DominantTraits: traits,
@ -1077,7 +1101,7 @@ export const addKubrowPet = (
Configs: configs,
XP: 0,
Details: details,
IsNew: true
IsNew: inventory.KubrowPets.find(x => x.ItemType == kubrowPetName) ? undefined : true
}) - 1;
inventoryChanges.KubrowPets ??= [];
inventoryChanges.KubrowPets.push(inventory.KubrowPets[kubrowPetIndex].toJSON<IEquipmentClient>());
@ -1105,24 +1129,29 @@ const isCurrencyTracked = (usePremium: boolean): boolean => {
export const updateCurrency = (
inventory: TInventoryDatabaseDocument,
price: number,
usePremium: boolean
usePremium: boolean,
inventoryChanges: IInventoryChanges = {}
): IInventoryChanges => {
const currencyChanges: IInventoryChanges = {};
if (price != 0 && isCurrencyTracked(usePremium)) {
if (usePremium) {
if (inventory.PremiumCreditsFree > 0) {
currencyChanges.PremiumCreditsFree = Math.min(price, inventory.PremiumCreditsFree) * -1;
inventory.PremiumCreditsFree += currencyChanges.PremiumCreditsFree;
const premiumCreditsFreeDelta = Math.min(price, inventory.PremiumCreditsFree) * -1;
inventoryChanges.PremiumCreditsFree ??= 0;
inventoryChanges.PremiumCreditsFree += premiumCreditsFreeDelta;
inventory.PremiumCreditsFree += premiumCreditsFreeDelta;
}
currencyChanges.PremiumCredits = -price;
inventory.PremiumCredits += currencyChanges.PremiumCredits;
inventoryChanges.PremiumCredits ??= 0;
inventoryChanges.PremiumCredits -= price;
inventory.PremiumCredits -= price;
logger.debug(`currency changes `, { PremiumCredits: -price });
} else {
currencyChanges.RegularCredits = -price;
inventory.RegularCredits += currencyChanges.RegularCredits;
inventoryChanges.RegularCredits ??= 0;
inventoryChanges.RegularCredits -= price;
inventory.RegularCredits -= price;
logger.debug(`currency changes `, { RegularCredits: -price });
}
logger.debug(`currency changes `, currencyChanges);
}
return currencyChanges;
return inventoryChanges;
};
export const addFusionPoints = (inventory: TInventoryDatabaseDocument, add: number): number => {
@ -1173,8 +1202,10 @@ export const addStanding = (
inventory: TInventoryDatabaseDocument,
syndicateTag: string,
gainedStanding: number,
isMedallion: boolean = false
): IAffiliationMods => {
affiliationMods: IAffiliationMods[] = [],
isMedallion: boolean = false,
propagateAlignments: boolean = true
): void => {
let syndicate = inventory.Affiliations.find(x => x.Tag == syndicateTag);
const syndicateMeta = ExportSyndicates[syndicateTag];
@ -1186,6 +1217,10 @@ export const addStanding = (
const max = getMaxStanding(syndicateMeta, syndicate.Title ?? 0);
if (syndicate.Standing + gainedStanding > max) gainedStanding = max - syndicate.Standing;
if (syndicate.Title == -2 && syndicate.Standing + gainedStanding < -71000) {
gainedStanding = -71000 + syndicate.Standing;
}
if (!isMedallion || syndicateMeta.medallionsCappedByDailyLimit) {
if (gainedStanding > getStandingLimit(inventory, syndicateMeta.dailyLimitBin)) {
gainedStanding = getStandingLimit(inventory, syndicateMeta.dailyLimitBin);
@ -1194,10 +1229,27 @@ export const addStanding = (
}
syndicate.Standing += gainedStanding;
return {
const affiliationMod: IAffiliationMods = {
Tag: syndicateTag,
Standing: gainedStanding
};
affiliationMods.push(affiliationMod);
if (syndicateMeta.alignments) {
if (propagateAlignments) {
for (const [tag, factor] of Object.entries(syndicateMeta.alignments)) {
addStanding(inventory, tag, gainedStanding * factor, affiliationMods, isMedallion, false);
}
} else {
while (syndicate.Standing < getMinStanding(syndicateMeta, syndicate.Title ?? 0)) {
syndicate.Title ??= 0;
syndicate.Title -= 1;
affiliationMod.Title ??= 0;
affiliationMod.Title -= 1;
logger.debug(`${syndicateTag} is decreasing to title ${syndicate.Title} after applying alignment`);
}
}
}
};
// TODO: AffiliationMods support (Nightwave).
@ -1257,6 +1309,9 @@ export const addEquipment = (
},
defaultOverwrites
);
if (equipment.IsNew) {
equipment.IsNew = !inventory[category].find(x => x.ItemType == type);
}
if (!equipment.IsNew) {
equipment.IsNew = undefined;
}
@ -1491,9 +1546,10 @@ export const applyClientEquipmentUpdates = (
const category = inventory[categoryName];
gearArray.forEach(({ ItemId, XP, InfestationDate }) => {
const item = category.id(ItemId.$oid);
const item = category.id(fromOid(ItemId));
if (!item) {
throw new Error(`No item with id ${ItemId.$oid} in ${categoryName}`);
logger.warn(`Skipping unknown ${categoryName} item: id ${fromOid(ItemId)} not found`);
return;
}
if (XP) {
@ -1569,12 +1625,17 @@ export const addMiscItems = (inventory: TInventoryDatabaseDocument, itemsArray:
if (MiscItems[itemIndex].ItemCount == 0) {
MiscItems.splice(itemIndex, 1);
} else if (MiscItems[itemIndex].ItemCount <= 0) {
logger.warn(`account now owns a negative amount of ${ItemType}`);
logger.warn(`inventory.MiscItems has a negative count for ${ItemType}`);
}
});
};
const applyArrayChanges = (arr: ITypeCount[], changes: ITypeCount[]): void => {
const applyArrayChanges = (
inventory: TInventoryDatabaseDocument,
key: "ShipDecorations" | "Consumables" | "CrewShipRawSalvage" | "CrewShipAmmo" | "Recipes" | "LevelKeys",
changes: ITypeCount[]
): void => {
const arr: ITypeCount[] = inventory[key];
for (const change of changes) {
if (change.ItemCount != 0) {
let itemIndex = arr.findIndex(x => x.ItemType === change.ItemType);
@ -1586,34 +1647,34 @@ const applyArrayChanges = (arr: ITypeCount[], changes: ITypeCount[]): void => {
if (arr[itemIndex].ItemCount == 0) {
arr.splice(itemIndex, 1);
} else if (arr[itemIndex].ItemCount <= 0) {
logger.warn(`account now owns a negative amount of ${change.ItemType}`);
logger.warn(`inventory.${key} has a negative count for ${change.ItemType}`);
}
}
}
};
export const addShipDecorations = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => {
applyArrayChanges(inventory.ShipDecorations, itemsArray);
applyArrayChanges(inventory, "ShipDecorations", itemsArray);
};
export const addConsumables = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => {
applyArrayChanges(inventory.Consumables, itemsArray);
applyArrayChanges(inventory, "Consumables", itemsArray);
};
export const addCrewShipRawSalvage = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => {
applyArrayChanges(inventory.CrewShipRawSalvage, itemsArray);
applyArrayChanges(inventory, "CrewShipRawSalvage", itemsArray);
};
export const addCrewShipAmmo = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => {
applyArrayChanges(inventory.CrewShipAmmo, itemsArray);
applyArrayChanges(inventory, "CrewShipAmmo", itemsArray);
};
export const addRecipes = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => {
applyArrayChanges(inventory.Recipes, itemsArray);
applyArrayChanges(inventory, "Recipes", itemsArray);
};
export const addLevelKeys = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => {
applyArrayChanges(inventory.LevelKeys, itemsArray);
applyArrayChanges(inventory, "LevelKeys", itemsArray);
};
export const addMods = (inventory: TInventoryDatabaseDocument, itemsArray: IRawUpgrade[]): void => {
@ -1633,7 +1694,7 @@ export const addMods = (inventory: TInventoryDatabaseDocument, itemsArray: IRawU
if (RawUpgrades[itemIndex].ItemCount == 0) {
RawUpgrades.splice(itemIndex, 1);
} else if (RawUpgrades[itemIndex].ItemCount <= 0) {
logger.warn(`account now owns a negative amount of ${ItemType}`);
logger.warn(`inventory.RawUpgrades has a negative count for ${ItemType}`);
}
});
};
@ -1648,7 +1709,7 @@ export const addFusionTreasures = (inventory: TInventoryDatabaseDocument, itemsA
if (FusionTreasures[itemIndex].ItemCount == 0) {
FusionTreasures.splice(itemIndex, 1);
} else if (FusionTreasures[itemIndex].ItemCount <= 0) {
logger.warn(`account now owns a negative amount of ${ItemType}`);
logger.warn(`inventory.FusionTreasures has a negative count for ${ItemType}`);
}
} else {
FusionTreasures.push({ ItemCount, ItemType, Sockets });
@ -1694,6 +1755,7 @@ export const addLoreFragmentScans = (inventory: TInventoryDatabaseDocument, arr:
};
export const addChallenges = (
account: TAccountDocument,
inventory: TInventoryDatabaseDocument,
ChallengeProgress: IChallengeProgress[],
SeasonChallengeCompletions: ISeasonChallenge[] | undefined
@ -1716,26 +1778,33 @@ export const addChallenges = (
continue;
}
const meta = ExportNightwave.challenges[challenge.challenge];
logger.debug("Completed challenge", meta);
const meta = ExportChallenges[challenge.challenge];
const nightwaveSyndicateTag = getNightwaveSyndicateTag(account.BuildLabel);
logger.debug("Completed season challenge", {
uniqueName: challenge.challenge,
syndicateTag: nightwaveSyndicateTag,
...meta
});
if (nightwaveSyndicateTag) {
let affiliation = inventory.Affiliations.find(x => x.Tag == nightwaveSyndicateTag);
if (!affiliation) {
affiliation =
inventory.Affiliations[
inventory.Affiliations.push({
Tag: nightwaveSyndicateTag,
Standing: 0
}) - 1
];
}
let affiliation = inventory.Affiliations.find(x => x.Tag == ExportNightwave.affiliationTag);
if (!affiliation) {
affiliation =
inventory.Affiliations[
inventory.Affiliations.push({
Tag: ExportNightwave.affiliationTag,
Standing: 0
}) - 1
];
const standingToAdd = meta.standing! * (config.nightwaveStandingMultiplier ?? 1);
affiliation.Standing += standingToAdd;
if (affiliationMods.length == 0) {
affiliationMods.push({ Tag: nightwaveSyndicateTag });
}
affiliationMods[0].Standing ??= 0;
affiliationMods[0].Standing += standingToAdd;
}
affiliation.Standing += meta.standing;
if (affiliationMods.length == 0) {
affiliationMods.push({ Tag: ExportNightwave.affiliationTag });
}
affiliationMods[0].Standing ??= 0;
affiliationMods[0].Standing += meta.standing;
}
}
return affiliationMods;
@ -1885,6 +1954,29 @@ export const cleanupInventory = (inventory: TInventoryDatabaseDocument): void =>
}
};
export const getDialogue = (inventory: TInventoryDatabaseDocument, dialogueName: string): IDialogueDatabase => {
let dialogue = inventory.DialogueHistory!.Dialogues!.find(x => x.DialogueName == dialogueName);
if (!dialogue) {
dialogue =
inventory.DialogueHistory!.Dialogues![
inventory.DialogueHistory!.Dialogues!.push({
Rank: 0,
Chemistry: 0,
AvailableDate: new Date(0),
AvailableGiftDate: new Date(0),
RankUpExpiry: new Date(0),
BountyChemExpiry: new Date(0),
QueuedDialogues: [],
Gifts: [],
Booleans: [],
Completed: [],
DialogueName: dialogueName
}) - 1
];
}
return dialogue;
};
export const getCalendarProgress = (inventory: TInventoryDatabaseDocument): ICalendarProgress => {
const currentSeason = getWorldState().KnownCalendarSeasons[0];
@ -1924,8 +2016,7 @@ export const giveNemesisWeaponRecipe = (
weaponType: string,
nemesisName: string = "AGOR ROK",
weaponLoc?: string,
KillingSuit: string = "/Lotus/Powersuits/Ember/Ember",
fp: bigint = generateRewardSeed()
profile: INemesisProfile = generateNemesisProfile()
): void => {
if (!weaponLoc) {
weaponLoc = ExportWeapons[weaponType].name;
@ -1946,8 +2037,8 @@ export const giveNemesisWeaponRecipe = (
compat: weaponType,
buffs: [
{
Tag: getInnateDamageTag(KillingSuit),
Value: getInnateDamageValue(fp)
Tag: profile.innateDamageTag,
Value: profile.innateDamageValue
}
]
},
@ -1956,27 +2047,15 @@ export const giveNemesisWeaponRecipe = (
});
};
export const giveNemesisPetRecipe = (inventory: TInventoryDatabaseDocument, nemesisName: string = "AGOR ROK"): void => {
const head = getRandomElement([
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadA",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadB",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadC"
])!;
const body = getRandomElement([
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyA",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyB",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyC"
])!;
const legs = getRandomElement([
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsA",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsB",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsC"
])!;
const tail = getRandomElement([
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailA",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailB",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailC"
])!;
export const giveNemesisPetRecipe = (
inventory: TInventoryDatabaseDocument,
nemesisName: string = "AGOR ROK",
profile: INemesisProfile = generateNemesisProfile()
): void => {
const head = profile.petHead!;
const body = profile.petBody!;
const legs = profile.petLegs!;
const tail = profile.petTail!;
const recipeType = Object.entries(ExportRecipes).find(arr => arr[1].resultType == head)![0];
addRecipes(inventory, [
{
@ -1993,3 +2072,7 @@ export const giveNemesisPetRecipe = (inventory: TInventoryDatabaseDocument, neme
} satisfies INemesisPetTargetFingerprint)
});
};
export const getEffectiveAvatarImageType = (inventory: TInventoryDatabaseDocument): string => {
return inventory.ActiveAvatarImageType ?? "/Lotus/Types/StoreItems/AvatarImages/AvatarImageDefault";
};

View File

@ -1,5 +1,4 @@
import { IKeyChainRequest } from "@/src/types/requestTypes";
import { getIndexAfter } from "@/src/helpers/stringHelpers";
import {
dict_de,
dict_en,
@ -53,20 +52,32 @@ export const getRecipeByResult = (resultType: string): IRecipe | undefined => {
return Object.values(ExportRecipes).find(x => x.resultType == resultType);
};
export const getItemCategoryByUniqueName = (uniqueName: string): string => {
//Lotus/Types/Items/MiscItems/PolymerBundle
let splitWord = "Items/";
if (!uniqueName.includes("/Items/")) {
splitWord = "/Types/";
export const getItemCategoryByUniqueName = (uniqueName: string): string | undefined => {
if (uniqueName in ExportCustoms) {
return ExportCustoms[uniqueName].productCategory;
}
const index = getIndexAfter(uniqueName, splitWord);
if (index === -1) {
throw new Error(`error parsing item category ${uniqueName}`);
if (uniqueName in ExportDrones) {
return "Drones";
}
const category = uniqueName.substring(index).split("/")[0];
return category;
if (uniqueName in ExportKeys) {
return "LevelKeys";
}
if (uniqueName in ExportGear) {
return "Consumables";
}
if (uniqueName in ExportResources) {
return ExportResources[uniqueName].productCategory;
}
if (uniqueName in ExportSentinels) {
return ExportSentinels[uniqueName].productCategory;
}
if (uniqueName in ExportWarframes) {
return ExportWarframes[uniqueName].productCategory;
}
if (uniqueName in ExportWeapons) {
return ExportWeapons[uniqueName].productCategory;
}
return undefined;
};
export const getItemName = (uniqueName: string): string | undefined => {
@ -222,7 +233,7 @@ export const isStoreItem = (type: string): boolean => {
};
export const toStoreItem = (type: string): string => {
if (type.startsWith("/Lotus/Types/StoreItems/Boosters/")) {
if (type.startsWith("/Lotus/Types/Boosters/")) {
const boosterEntry = Object.entries(ExportBoosters).find(arr => arr[1].typeName == type);
if (boosterEntry) {
return boosterEntry[0];

View File

@ -1,11 +1,17 @@
import randomRewards from "@/static/fixed_responses/loginRewards/randomRewards.json";
import { IInventoryChanges } from "../types/purchaseTypes";
import { TAccountDocument } from "./loginService";
import { CRng, mixSeeds } from "./rngService";
import { mixSeeds, SRng } from "./rngService";
import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel";
import { addBooster, updateCurrency } from "./inventoryService";
import { handleStoreItemAcquisition } from "./purchaseService";
import { ExportBoosters, ExportRecipes, ExportWarframes, ExportWeapons } from "warframe-public-export-plus";
import {
ExportBoosterPacks,
ExportBoosters,
ExportRecipes,
ExportWarframes,
ExportWeapons
} from "warframe-public-export-plus";
import { toStoreItem } from "./itemDataService";
export interface ILoginRewardsReponse {
@ -49,8 +55,8 @@ const scaleAmount = (day: number, amount: number, scalingMultiplier: number): nu
// Always produces the same result for the same account _id & LoginDays pair.
export const isLoginRewardAChoice = (account: TAccountDocument): boolean => {
const accountSeed = parseInt(account._id.toString().substring(16), 16);
const rng = new CRng(mixSeeds(accountSeed, account.LoginDays));
return rng.random() < 0.25; // Using 25% as an approximate chance for pick-a-doors. More conclusive data analysis is needed.
const rng = new SRng(mixSeeds(accountSeed, account.LoginDays));
return rng.randomFloat() < 0.25;
};
// Always produces the same result for the same account _id & LoginDays pair.
@ -59,8 +65,8 @@ export const getRandomLoginRewards = (
inventory: TInventoryDatabaseDocument
): ILoginReward[] => {
const accountSeed = parseInt(account._id.toString().substring(16), 16);
const rng = new CRng(mixSeeds(accountSeed, account.LoginDays));
const pick_a_door = rng.random() < 0.25; // Using 25% as an approximate chance for pick-a-doors. More conclusive data analysis is needed.
const rng = new SRng(mixSeeds(accountSeed, account.LoginDays));
const pick_a_door = rng.randomFloat() < 0.25;
const rewards = [getRandomLoginReward(rng, account.LoginDays, inventory)];
if (pick_a_door) {
do {
@ -73,9 +79,10 @@ export const getRandomLoginRewards = (
return rewards;
};
const getRandomLoginReward = (rng: CRng, day: number, inventory: TInventoryDatabaseDocument): ILoginReward => {
const getRandomLoginReward = (rng: SRng, day: number, inventory: TInventoryDatabaseDocument): ILoginReward => {
const reward = rng.randomReward(randomRewards)!;
//const reward = randomRewards.find(x => x.RewardType == "RT_BOOSTER")!;
let storeItemType: string = reward.StoreItemType;
if (reward.RewardType == "RT_RANDOM_RECIPE") {
const masteredItems = new Set();
for (const entry of inventory.XPInfo) {
@ -102,7 +109,12 @@ const getRandomLoginReward = (rng: CRng, day: number, inventory: TInventoryDatab
// This account has all applicable warframes and weapons already mastered (filthy cheater), need a different reward.
return getRandomLoginReward(rng, day, inventory);
}
reward.StoreItemType = toStoreItem(rng.randomElement(eligibleRecipes)!);
storeItemType = toStoreItem(rng.randomElement(eligibleRecipes)!);
} else if (reward.StoreItemType == "/Lotus/StoreItems/Types/BoosterPacks/LoginRewardRandomProjection") {
storeItemType = toStoreItem(
rng.randomElement(ExportBoosterPacks["/Lotus/Types/BoosterPacks/LoginRewardRandomProjection"].components)!
.Item
);
}
return {
//_id: toOid(new Types.ObjectId()),
@ -110,7 +122,7 @@ const getRandomLoginReward = (rng: CRng, day: number, inventory: TInventoryDatab
//CouponType: "CPT_PLATINUM",
Icon: reward.Icon ?? "",
//ItemType: "",
StoreItemType: reward.StoreItemType,
StoreItemType: storeItemType,
//ProductCategory: "Pistols",
Amount: reward.Duration ? 1 : Math.round(scaleAmount(day, reward.Amount, reward.ScalingMultiplier)),
ScalingMultiplier: reward.ScalingMultiplier,

View File

@ -74,7 +74,7 @@ export const getAccountForRequest = async (req: Request): Promise<TAccountDocume
throw new Error("Request is missing nonce parameter");
}
const account = await Account.findById(req.query.accountId);
const account = await Account.findById(req.query.accountId as string);
if (!account || account.Nonce != nonce) {
throw new Error("Invalid accountId-nonce pair");
}
@ -90,7 +90,7 @@ export const getAccountIdForRequest = async (req: Request): Promise<string> => {
};
export const isAdministrator = (account: TAccountDocument): boolean => {
return !!config.administratorNames?.find(x => x == account.DisplayName);
return config.administratorNames?.indexOf(account.DisplayName) != -1;
};
const platform_magics = [753, 639, 247, 37, 60];

View File

@ -10,7 +10,7 @@ import {
import { IMissionInventoryUpdateRequest, IRewardInfo } from "../types/requestTypes";
import { logger } from "@/src/utils/logger";
import { IRngResult, SRng, getRandomElement, getRandomReward } from "@/src/services/rngService";
import { equipmentKeys, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
import { equipmentKeys, IMission, ITypeCount, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
import {
addBooster,
addChallenges,
@ -35,6 +35,7 @@ import {
combineInventoryChanges,
generateRewardSeed,
getCalendarProgress,
getDialogue,
giveNemesisPetRecipe,
giveNemesisWeaponRecipe,
updateCurrency,
@ -43,7 +44,7 @@ import {
import { updateQuestKey } from "@/src/services/questService";
import { Types } from "mongoose";
import { IAffiliationMods, IInventoryChanges } from "@/src/types/purchaseTypes";
import { getLevelKeyRewards, toStoreItem } from "@/src/services/itemDataService";
import { fromStoreItem, getLevelKeyRewards, toStoreItem } from "@/src/services/itemDataService";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { getEntriesUnsafe } from "@/src/utils/ts-utils";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
@ -55,12 +56,30 @@ import kuriaMessage50 from "@/static/fixed_responses/kuriaMessages/fiftyPercent.
import kuriaMessage75 from "@/static/fixed_responses/kuriaMessages/seventyFivePercent.json";
import kuriaMessage100 from "@/static/fixed_responses/kuriaMessages/oneHundredPercent.json";
import conservationAnimals from "@/static/fixed_responses/conservationAnimals.json";
import { getInfNodes, getWeaponsForManifest, sendCodaFinishedMessage } from "@/src/helpers/nemesisHelpers";
import {
generateNemesisProfile,
getInfestedLichItemRewards,
getInfNodes,
getKillTokenRewardCount,
getNemesisManifest,
getNemesisPasscode
} from "@/src/helpers/nemesisHelpers";
import { Loadout } from "../models/inventoryModels/loadoutModel";
import { ILoadoutConfigDatabase } from "../types/saveLoadoutTypes";
import { getLiteSortie, getWorldState, idToWeek } from "./worldStateService";
import {
getLiteSortie,
getSortie,
getWorldState,
idToBountyCycle,
idToDay,
idToWeek,
pushClassicBounties
} from "./worldStateService";
import { config } from "./configService";
import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json";
import { ISyndicateMissionInfo } from "../types/worldStateTypes";
import { fromOid } from "../helpers/inventoryHelpers";
import { TAccountDocument } from "./loginService";
const getRotations = (rewardInfo: IRewardInfo, tierOverride?: number): number[] => {
// For Spy missions, e.g. 3 vaults cracked = A, B, C
@ -117,6 +136,7 @@ const getRandomRewardByChance = (pool: IReward[], rng?: SRng): IRngResult | unde
//const knownUnhandledKeys: readonly string[] = ["test"] as const; // for unimplemented but important keys
export const addMissionInventoryUpdates = async (
account: TAccountDocument,
inventory: TInventoryDatabaseDocument,
inventoryUpdates: IMissionInventoryUpdateRequest
): Promise<IInventoryChanges> => {
@ -166,6 +186,14 @@ export const addMissionInventoryUpdates = async (
}
if (inventoryUpdates.RewardInfo.NemesisHintProgress && inventory.Nemesis) {
inventory.Nemesis.HintProgress += inventoryUpdates.RewardInfo.NemesisHintProgress;
if (inventory.Nemesis.Faction != "FC_INFESTATION" && inventory.Nemesis.Hints.length != 3) {
const progressNeeded = [35, 60, 100][inventory.Nemesis.Hints.length];
if (inventory.Nemesis.HintProgress >= progressNeeded) {
inventory.Nemesis.HintProgress -= progressNeeded;
const passcode = getNemesisPasscode(inventory.Nemesis);
inventory.Nemesis.Hints.push(passcode[inventory.Nemesis.Hints.length]);
}
}
}
if (inventoryUpdates.MissionStatus == "GS_SUCCESS" && inventoryUpdates.RewardInfo.jobId) {
// e.g. for Profit-Taker Phase 1:
@ -261,7 +289,7 @@ export const addMissionInventoryUpdates = async (
addRecipes(inventory, value);
break;
case "ChallengeProgress":
addChallenges(inventory, value, inventoryUpdates.SeasonChallengeCompletions);
addChallenges(account, inventory, value, inventoryUpdates.SeasonChallengeCompletions);
break;
case "FusionTreasures":
addFusionTreasures(inventory, value);
@ -298,8 +326,8 @@ export const addMissionInventoryUpdates = async (
break;
}
case "PlayerSkillGains": {
inventory.PlayerSkills.LPP_SPACE += value.LPP_SPACE;
inventory.PlayerSkills.LPP_DRIFTER += value.LPP_DRIFTER;
inventory.PlayerSkills.LPP_SPACE += value.LPP_SPACE ?? 0;
inventory.PlayerSkills.LPP_DRIFTER += value.LPP_DRIFTER ?? 0;
break;
}
case "CustomMarkers": {
@ -356,7 +384,7 @@ export const addMissionInventoryUpdates = async (
}
if (
inventory.LibraryActiveDailyTaskInfo &&
inventory.LibraryActiveDailyTaskInfo.EnemyTypes.find(x => x == scan.EnemyType)
inventory.LibraryActiveDailyTaskInfo.EnemyTypes.indexOf(scan.EnemyType) != -1
) {
inventory.LibraryActiveDailyTaskInfo.Scans ??= 0;
inventory.LibraryActiveDailyTaskInfo.Scans += scan.Count;
@ -398,8 +426,14 @@ export const addMissionInventoryUpdates = async (
break;
case "Upgrades":
value.forEach(clientUpgrade => {
const upgrade = inventory.Upgrades.id(clientUpgrade.ItemId.$oid)!;
upgrade.UpgradeFingerprint = clientUpgrade.UpgradeFingerprint; // primitive way to copy over the riven challenge progress
const id = fromOid(clientUpgrade.ItemId);
if (id == "") {
// U19 does not provide RawUpgrades and instead interleaves them with riven progress here
addMods(inventory, [clientUpgrade]);
} else {
const upgrade = inventory.Upgrades.id(id)!;
upgrade.UpgradeFingerprint = clientUpgrade.UpgradeFingerprint; // primitive way to copy over the riven challenge progress
}
});
break;
case "WeaponSkins":
@ -413,7 +447,9 @@ export const addMissionInventoryUpdates = async (
});
break;
case "SyndicateId": {
inventory.CompletedSyndicates.push(value);
if (!config.syndicateMissionsRepeatable) {
inventory.CompletedSyndicates.push(value);
}
break;
}
case "SortieId": {
@ -623,37 +659,98 @@ export const addMissionInventoryUpdates = async (
Rank: inventory.Nemesis.Rank,
Traded: inventory.Nemesis.Traded,
PrevOwners: inventory.Nemesis.PrevOwners,
SecondInCommand: inventory.Nemesis.SecondInCommand,
SecondInCommand: false,
Weakened: inventory.Nemesis.Weakened,
// And set killed flag
k: value.killed
});
const manifest = getNemesisManifest(inventory.Nemesis.manifest);
const profile = generateNemesisProfile(
inventory.Nemesis.fp,
manifest,
inventory.Nemesis.KillingSuit
);
const att: string[] = [];
let countedAtt: ITypeCount[] | undefined;
if (value.killed) {
if (
value.weaponLoc &&
inventory.Nemesis.Faction != "FC_INFESTATION" // weaponLoc is "/Lotus/Language/Weapons/DerelictCernosName" for these for some reason
) {
const weaponType = getWeaponsForManifest(inventory.Nemesis.manifest)[
inventory.Nemesis.WeaponIdx
];
giveNemesisWeaponRecipe(
inventory,
weaponType,
value.nemesisName,
value.weaponLoc,
inventory.Nemesis.KillingSuit,
inventory.Nemesis.fp
);
const weaponType = manifest.weapons[inventory.Nemesis.WeaponIdx];
giveNemesisWeaponRecipe(inventory, weaponType, value.nemesisName, value.weaponLoc, profile);
att.push(weaponType);
}
if (value.petLoc) {
giveNemesisPetRecipe(inventory);
//if (value.petLoc) {
if (profile.petHead) {
giveNemesisPetRecipe(inventory, value.nemesisName, profile);
att.push(
{
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadA":
"/Lotus/Types/Recipes/ZanukaPet/ZanukaPetCompleteHeadABlueprint",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadB":
"/Lotus/Types/Recipes/ZanukaPet/ZanukaPetCompleteHeadBBlueprint",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadC":
"/Lotus/Types/Recipes/ZanukaPet/ZanukaPetCompleteHeadCBlueprint"
}[profile.petHead]
);
}
}
// TOVERIFY: Is the inbox message also sent when converting a lich? If not, how are the rewards given?
// "Players will receive a Lich's Ephemera regardless of whether they Vanquish or Convert them."
if (profile.ephemera) {
addSkin(inventory, profile.ephemera);
att.push(profile.ephemera);
}
const skinRewardStoreItem = value.killed ? manifest.firstKillReward : manifest.firstConvertReward;
if (Object.keys(addSkin(inventory, fromStoreItem(skinRewardStoreItem))).length != 0) {
att.push(skinRewardStoreItem);
}
if (inventory.Nemesis.Faction == "FC_INFESTATION") {
await sendCodaFinishedMessage(inventory, inventory.Nemesis.fp, value.nemesisName, value.killed);
const [rotARewardStoreItem, rotBRewardStoreItem] = getInfestedLichItemRewards(
inventory.Nemesis.fp
);
const rotAReward = fromStoreItem(rotARewardStoreItem);
const rotBReward = fromStoreItem(rotBRewardStoreItem);
await addItem(inventory, rotAReward);
await addItem(inventory, rotBReward);
att.push(rotAReward);
att.push(rotBReward);
if (value.killed) {
countedAtt = [
{
ItemType: "/Lotus/Types/Items/MiscItems/CodaWeaponBucks",
ItemCount: getKillTokenRewardCount(inventory.Nemesis.fp)
}
];
addMiscItems(inventory, countedAtt);
}
}
if (value.killed) {
await createMessage(inventory.accountOwnerId, [
{
sndr: "/Lotus/Language/Bosses/Ordis",
msg: manifest.messageBody,
arg: [
{
Key: "LICH_NAME",
Tag: value.nemesisName
}
],
att: att,
countedAtt: countedAtt,
attVisualOnly: true,
sub: manifest.messageTitle,
icon: "/Lotus/Interface/Icons/Npcs/Ordis.png",
highPriority: true
}
]);
}
inventory.Nemesis = undefined;
@ -817,6 +914,13 @@ const hexConquestRewards: IConquestReward[] = [
}
];
const droptableAliases: Record<string, string> = {
"/Lotus/Types/DropTables/ManInTheWall/MITWGruzzlingArcanesDropTable":
"/Lotus/Types/DropTables/EntratiLabDropTables/DoppelgangerDropTable",
"/Lotus/Types/DropTables/WF1999DropTables/LasrianTankSteelPathDropTable":
"/Lotus/Types/DropTables/WF1999DropTables/LasrianTankHardModeDropTable"
};
//TODO: return type of partial missioninventoryupdate response
export const addMissionRewards = async (
inventory: TInventoryDatabaseDocument,
@ -839,7 +943,13 @@ export const addMissionRewards = async (
}
//TODO: check double reward merging
const MissionRewards: IMissionReward[] = getRandomMissionDrops(inventory, rewardInfo, wagerTier, firstCompletion);
const MissionRewards: IMissionReward[] = getRandomMissionDrops(
inventory,
rewardInfo,
missions,
wagerTier,
firstCompletion
);
logger.debug("random mission drops:", MissionRewards);
const inventoryChanges: IInventoryChanges = {};
const AffiliationMods: IAffiliationMods[] = [];
@ -924,6 +1034,14 @@ export const addMissionRewards = async (
});
}
if (rewardInfo.periodicMissionTag == "EliteAlert" || rewardInfo.periodicMissionTag == "EliteAlertB") {
missionCompletionCredits += 50_000;
MissionRewards.push({
StoreItem: "/Lotus/StoreItems/Types/Items/MiscItems/Elitium",
ItemCount: 1
});
}
if (rewardInfo.ConquestCompleted !== undefined) {
let score = 1;
if (rewardInfo.ConquestHardModeActive === 1) score += 3;
@ -1011,11 +1129,9 @@ export const addMissionRewards = async (
if (strippedItems) {
for (const si of strippedItems) {
if (si.DropTable == "/Lotus/Types/DropTables/ManInTheWall/MITWGruzzlingArcanesDropTable") {
logger.debug(
`rewriting ${si.DropTable} to /Lotus/Types/DropTables/EntratiLabDropTables/DoppelgangerDropTable`
);
si.DropTable = "/Lotus/Types/DropTables/EntratiLabDropTables/DoppelgangerDropTable";
if (si.DropTable in droptableAliases) {
logger.debug(`rewriting ${si.DropTable} to ${droptableAliases[si.DropTable]}`);
si.DropTable = droptableAliases[si.DropTable];
}
const droptables = ExportEnemies.droptables[si.DropTable] ?? [];
if (si.DROP_MOD) {
@ -1066,11 +1182,12 @@ export const addMissionRewards = async (
if (nodeIndex !== -1) inventory.Nemesis.InfNodes.splice(nodeIndex, 1);
if (inventory.Nemesis.InfNodes.length <= 0) {
const manifest = getNemesisManifest(inventory.Nemesis.manifest);
if (inventory.Nemesis.Faction != "FC_INFESTATION") {
inventory.Nemesis.Rank = Math.min(inventory.Nemesis.Rank + 1, 4);
inventory.Nemesis.Rank = Math.min(inventory.Nemesis.Rank + 1, manifest.systemIndexes.length - 1);
inventoryChanges.Nemesis.Rank = inventory.Nemesis.Rank;
}
inventory.Nemesis.InfNodes = getInfNodes(inventory.Nemesis.Faction, inventory.Nemesis.Rank);
inventory.Nemesis.InfNodes = getInfNodes(manifest, inventory.Nemesis.Rank);
}
if (inventory.Nemesis.Faction == "FC_INFESTATION") {
@ -1086,16 +1203,18 @@ export const addMissionRewards = async (
if (rewardInfo.JobStage != undefined && rewardInfo.jobId) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [jobType, unkIndex, hubNode, syndicateId, locationTag] = rewardInfo.jobId.split("_");
const worldState = getWorldState();
let syndicateEntry = worldState.SyndicateMissions.find(m => m._id.$oid === syndicateId);
if (!syndicateEntry) syndicateEntry = worldState.SyndicateMissions.find(m => m.Tag === syndicateId); // Sometimes syndicateId can be tag
const [jobType, unkIndex, hubNode, syndicateMissionId, locationTag] = rewardInfo.jobId.split("_");
const syndicateMissions: ISyndicateMissionInfo[] = [];
if (syndicateMissionId) {
pushClassicBounties(syndicateMissions, idToBountyCycle(syndicateMissionId));
}
const syndicateEntry = syndicateMissions.find(m => m._id.$oid === syndicateMissionId);
if (syndicateEntry && syndicateEntry.Jobs) {
let currentJob = syndicateEntry.Jobs[rewardInfo.JobTier!];
if (syndicateEntry.Tag === "EntratiSyndicate") {
const vault = syndicateEntry.Jobs.find(j => j.locationTag === locationTag);
if (vault) currentJob = vault;
let medallionAmount = currentJob.xpAmounts[rewardInfo.JobStage];
let medallionAmount = Math.floor(currentJob.xpAmounts[rewardInfo.JobStage] / (rewardInfo.Q ? 0.8 : 1));
if (
["DeimosEndlessAreaDefenseBounty", "DeimosEndlessExcavateBounty", "DeimosEndlessPurifyBounty"].some(
@ -1117,15 +1236,18 @@ export const addMissionRewards = async (
SyndicateXPItemReward = medallionAmount;
} else {
if (rewardInfo.JobTier! >= 0) {
AffiliationMods.push(
addStanding(inventory, syndicateEntry.Tag, currentJob.xpAmounts[rewardInfo.JobStage])
addStanding(
inventory,
syndicateEntry.Tag,
Math.floor(currentJob.xpAmounts[rewardInfo.JobStage] / (rewardInfo.Q ? 0.8 : 1)),
AffiliationMods
);
} else {
if (jobType.endsWith("Heists/HeistProfitTakerBountyOne") && rewardInfo.JobStage === 2) {
AffiliationMods.push(addStanding(inventory, syndicateEntry.Tag, 1000));
addStanding(inventory, syndicateEntry.Tag, 1000, AffiliationMods);
}
if (jobType.endsWith("Hunts/AllTeralystsHunt") && rewardInfo.JobStage === 2) {
AffiliationMods.push(addStanding(inventory, syndicateEntry.Tag, 5000));
addStanding(inventory, syndicateEntry.Tag, 5000, AffiliationMods);
}
if (
[
@ -1136,7 +1258,7 @@ export const addMissionRewards = async (
"Heists/HeistExploiterBountyOne"
].some(ending => jobType.endsWith(ending))
) {
AffiliationMods.push(addStanding(inventory, syndicateEntry.Tag, 1000));
addStanding(inventory, syndicateEntry.Tag, 1000, AffiliationMods);
}
}
}
@ -1144,8 +1266,9 @@ export const addMissionRewards = async (
}
if (rewardInfo.challengeMissionId) {
const [syndicateTag, tierStr] = rewardInfo.challengeMissionId.split("_"); // TODO: third part in HexSyndicate jobs - Chemistry points
const [syndicateTag, tierStr, chemistryStr] = rewardInfo.challengeMissionId.split("_");
const tier = Number(tierStr);
const chemistry = Number(chemistryStr);
const isSteelPath = missions?.Tier;
if (syndicateTag === "ZarimanSyndicate") {
let medallionAmount = tier + 1;
@ -1160,7 +1283,24 @@ export const addMissionRewards = async (
let standingAmount = (tier + 1) * 1000;
if (tier > 5) standingAmount = 7500; // InfestedLichBounty
if (isSteelPath) standingAmount *= 1.5;
AffiliationMods.push(addStanding(inventory, syndicateTag, standingAmount));
addStanding(inventory, syndicateTag, standingAmount, AffiliationMods);
}
if (syndicateTag == "HexSyndicate" && chemistry && tier < 6) {
const seed = getWorldState().SyndicateMissions.find(x => x.Tag == "HexSyndicate")!.Seed;
const { nodes, buddies } = getHexBounties(seed);
const buddy = buddies[tier];
logger.debug(`Hex seed is ${seed}, giving chemistry for ${buddy}`);
if (missions?.Tag != nodes[tier]) {
logger.warn(
`Uh-oh, tier ${tier} bounty should've been on ${nodes[tier]} but you were just on ${missions?.Tag}`
);
}
const tomorrowAt0Utc = config.noKimCooldowns
? Date.now()
: (Math.trunc(Date.now() / 86400_000) + 1) * 86400_000;
const dialogue = getDialogue(inventory, buddy);
dialogue.Chemistry += chemistry;
dialogue.BountyChemExpiry = new Date(tomorrowAt0Utc);
}
if (isSteelPath) {
await addItem(inventory, "/Lotus/Types/Items/MiscItems/SteelEssence", 1);
@ -1276,6 +1416,7 @@ function getLevelCreditRewards(node: IRegion): number {
function getRandomMissionDrops(
inventory: TInventoryDatabaseDocument,
RewardInfo: IRewardInfo,
mission: IMission | undefined,
tierOverride: number | undefined,
firstCompletion: boolean
): IMissionReward[] {
@ -1357,8 +1498,50 @@ function getRandomMissionDrops(
// Invasion assassination has Phorid has the boss who should drop Nyx parts
// TODO: Check that the invasion faction is indeed FC_INFESTATION once the Invasions in worldState are more dynamic
rewardManifests = ["/Lotus/Types/Game/MissionDecks/BossMissionRewards/NyxRewards"];
} else if (RewardInfo.sortieId && region.missionIndex != 0) {
// Sortie mission types differ from the underlying node and hence also don't give rewards from the underlying nodes. Assassinations are an exception to this.
} else if (RewardInfo.sortieId) {
// Sortie mission types differ from the underlying node and hence also don't give rewards from the underlying nodes.
// Assassinations in non-lite sorties are an exception to this.
if (region.missionIndex == 0) {
const arr = RewardInfo.sortieId.split("_");
let giveNodeReward = false;
if (arr[1] != "Lite") {
const sortie = getSortie(idToDay(arr[1]));
giveNodeReward = sortie.Variants.find(x => x.node == arr[0])!.missionType == "MT_ASSASSINATION";
}
rewardManifests = giveNodeReward ? region.rewardManifests : [];
} else {
rewardManifests = [];
}
} else if (RewardInfo.T == 13) {
// Undercroft extra/side portal (normal mode), gives 1 Pathos Clamp + Duviri Arcane.
drops.push({
StoreItem: "/Lotus/StoreItems/Types/Gameplay/Duviri/Resource/DuviriDragonDropItem",
ItemCount: 1
});
rewardManifests = [
"/Lotus/Types/Game/MissionDecks/DuviriEncounterRewards/DuviriStaticUndercroftResourceRewards"
];
} else if (RewardInfo.T == 14) {
// Undercroft extra/side portal (steel path), gives 3 Pathos Clamps + Eidolon Arcane.
drops.push({
StoreItem: "/Lotus/StoreItems/Types/Gameplay/Duviri/Resource/DuviriDragonDropItem",
ItemCount: 3
});
rewardManifests = [
"/Lotus/Types/Game/MissionDecks/DuviriEncounterRewards/DuviriSteelPathStaticUndercroftResourceRewards"
];
} else if (RewardInfo.T == 15) {
rewardManifests = [
mission?.Tier == 1
? "/Lotus/Types/Game/MissionDecks/DuviriEncounterRewards/DuviriKullervoSteelPathRNGRewards"
: "/Lotus/Types/Game/MissionDecks/DuviriEncounterRewards/DuviriKullervoNormalRNGRewards"
];
} else if (RewardInfo.T == 70) {
// Orowyrm chest, gives 10 Pathos Clamps, or 15 on Steel Path.
drops.push({
StoreItem: "/Lotus/StoreItems/Types/Gameplay/Duviri/Resource/DuviriDragonDropItem",
ItemCount: mission?.Tier == 1 ? 15 : 10
});
rewardManifests = [];
} else {
rewardManifests = region.rewardManifests;
@ -1368,13 +1551,14 @@ function getRandomMissionDrops(
if (RewardInfo.jobId) {
if (RewardInfo.JobStage! >= 0) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [jobType, unkIndex, hubNode, syndicateId, locationTag] = RewardInfo.jobId.split("_");
const [jobType, unkIndex, hubNode, syndicateMissionId, locationTag] = RewardInfo.jobId.split("_");
let isEndlessJob = false;
if (syndicateId) {
const worldState = getWorldState();
let syndicateEntry = worldState.SyndicateMissions.find(m => m._id.$oid === syndicateId);
if (!syndicateEntry) syndicateEntry = worldState.SyndicateMissions.find(m => m.Tag === syndicateId);
if (syndicateMissionId) {
const syndicateMissions: ISyndicateMissionInfo[] = [];
if (syndicateMissionId) {
pushClassicBounties(syndicateMissions, idToBountyCycle(syndicateMissionId));
}
const syndicateEntry = syndicateMissions.find(m => m._id.$oid === syndicateMissionId);
if (syndicateEntry && syndicateEntry.Jobs) {
let job = syndicateEntry.Jobs[RewardInfo.JobTier!];
@ -1452,7 +1636,7 @@ function getRandomMissionDrops(
if (job.xpAmounts.length > 1) {
rotations = [RewardInfo.JobStage! % (job.xpAmounts.length - 1)];
} else {
rotations = [RewardInfo.JobStage!];
rotations = [0];
}
if (
RewardInfo.Q &&
@ -1480,7 +1664,7 @@ function getRandomMissionDrops(
ZarimanSyndicate: [
"/Lotus/Types/Game/MissionDecks/ZarimanJobMissionRewards/TierATableRewards",
"/Lotus/Types/Game/MissionDecks/ZarimanJobMissionRewards/TierBTableRewards",
"/Lotus/Types/Game/MissionDecks/ZarimanJobMissionRewards/TierCTableRewards",
"/Lotus/Types/Game/MissionDecks/ZarimanJobMissionRewards/TierCTableARewards", // [sic]
"/Lotus/Types/Game/MissionDecks/ZarimanJobMissionRewards/TierDTableRewards",
"/Lotus/Types/Game/MissionDecks/ZarimanJobMissionRewards/TierETableRewards"
],
@ -1507,6 +1691,35 @@ function getRandomMissionDrops(
logger.error(`Unknown syndicate or tier: ${RewardInfo.challengeMissionId}`);
}
} else {
if (RewardInfo.node == "SolNode238") {
// The Circuit
const category = mission?.Tier == 1 ? "EXC_HARD" : "EXC_NORMAL";
const progress = inventory.EndlessXP?.find(x => x.Category == category);
if (progress) {
// https://wiki.warframe.com/w/The%20Circuit#Tiers_and_Weekly_Rewards
const roundsCompleted = RewardInfo.rewardQualifications?.length || 0;
if (roundsCompleted >= 1) {
progress.Earn += 100;
}
if (roundsCompleted >= 2) {
progress.Earn += 110;
}
if (roundsCompleted >= 3) {
progress.Earn += 125;
}
if (roundsCompleted >= 4) {
progress.Earn += 145;
if (progress.BonusAvailable && progress.BonusAvailable.getTime() <= Date.now()) {
progress.Earn += 50;
progress.BonusAvailable = new Date(Date.now() + 24 * 3600_000); // TOVERIFY
}
}
if (roundsCompleted >= 5) {
progress.Earn += (roundsCompleted - 4) * 170;
}
}
tierOverride = 0;
}
rotations = getRotations(RewardInfo, tierOverride);
}
if (rewardManifests.length != 0) {
@ -1571,9 +1784,9 @@ function getRandomMissionDrops(
const drop = getRandomRewardByChance(
ExportRewards[
[
"/Lotus/Types/Game/MissionDecks/PurgatoryMissionRewards/PurgatoryBlackTokenRewards",
"/Lotus/Types/Game/MissionDecks/PurgatoryMissionRewards/PurgatoryBlueTokenRewards",
"/Lotus/Types/Game/MissionDecks/PurgatoryMissionRewards/PurgatoryGoldTokenRewards",
"/Lotus/Types/Game/MissionDecks/PurgatoryMissionRewards/PurgatoryBlueTokenRewards"
"/Lotus/Types/Game/MissionDecks/PurgatoryMissionRewards/PurgatoryBlackTokenRewards"
][Math.trunc(qualification / 3)]
][qualification % 3]
);
@ -1587,6 +1800,17 @@ function getRandomMissionDrops(
}
}
}
if (RewardInfo.periodicMissionTag?.startsWith("KuvaMission")) {
const drop = getRandomRewardByChance(
ExportRewards[
RewardInfo.periodicMissionTag == "KuvaMission6" || RewardInfo.periodicMissionTag == "KuvaMission12"
? "/Lotus/Types/Game/MissionDecks/KuvaMissionRewards/KuvaSiphonFloodRewards"
: "/Lotus/Types/Game/MissionDecks/KuvaMissionRewards/KuvaSiphonRewards"
][0]
)!;
drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount });
}
}
return drops;
}
@ -1639,3 +1863,55 @@ const libraryPersonalTargetToAvatar: Record<string, string> = {
"/Lotus/Types/Game/Library/Targets/Research10Target":
"/Lotus/Types/Enemies/Corpus/Spaceman/AIWeek/NullifySpacemanAvatar"
};
const node_excluded_buddies: Record<string, string> = {
SolNode856: "/Lotus/Types/Gameplay/1999Wf/Dialogue/ArthurDialogue_rom.dialogue",
SolNode852: "/Lotus/Types/Gameplay/1999Wf/Dialogue/LettieDialogue_rom.dialogue",
SolNode851: "/Lotus/Types/Gameplay/1999Wf/Dialogue/JabirDialogue_rom.dialogue",
SolNode850: "/Lotus/Types/Gameplay/1999Wf/Dialogue/EleanorDialogue_rom.dialogue",
SolNode853: "/Lotus/Types/Gameplay/1999Wf/Dialogue/AoiDialogue_rom.dialogue",
SolNode854: "/Lotus/Types/Gameplay/1999Wf/Dialogue/QuincyDialogue_rom.dialogue"
};
const getHexBounties = (seed: number): { nodes: string[]; buddies: string[] } => {
// We're gonna shuffle these arrays, so they're not truly 'const'.
const nodes: string[] = [
"SolNode850",
"SolNode851",
"SolNode852",
"SolNode853",
"SolNode854",
"SolNode856",
"SolNode858"
];
const excludable_nodes: string[] = ["SolNode851", "SolNode852", "SolNode853", "SolNode854"];
const buddies: string[] = [
"/Lotus/Types/Gameplay/1999Wf/Dialogue/JabirDialogue_rom.dialogue",
"/Lotus/Types/Gameplay/1999Wf/Dialogue/AoiDialogue_rom.dialogue",
"/Lotus/Types/Gameplay/1999Wf/Dialogue/ArthurDialogue_rom.dialogue",
"/Lotus/Types/Gameplay/1999Wf/Dialogue/EleanorDialogue_rom.dialogue",
"/Lotus/Types/Gameplay/1999Wf/Dialogue/LettieDialogue_rom.dialogue",
"/Lotus/Types/Gameplay/1999Wf/Dialogue/QuincyDialogue_rom.dialogue"
];
const rng = new SRng(seed);
rng.shuffleArray(nodes);
rng.shuffleArray(excludable_nodes);
while (nodes.length > buddies.length) {
nodes.splice(
nodes.findIndex(x => x == excludable_nodes[0]),
1
);
excludable_nodes.splice(0, 1);
}
rng.shuffleArray(buddies);
for (let i = 0; i != 6; ++i) {
if (buddies[i] == node_excluded_buddies[nodes[i]]) {
const swapIdx = (i + 1) % buddies.length;
const tmp = buddies[swapIdx];
buddies[swapIdx] = buddies[i];
buddies[i] = tmp;
}
}
return { nodes, buddies };
};

View File

@ -9,7 +9,7 @@ import {
updateSlots
} from "@/src/services/inventoryService";
import { getRandomWeightedRewardUc } from "@/src/services/rngService";
import { getVendorManifestByOid } from "@/src/services/serversideVendorsService";
import { applyStandingToVendorManifest, getVendorManifestByOid } from "@/src/services/serversideVendorsService";
import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
import { IPurchaseRequest, IPurchaseResponse, SlotPurchase, IInventoryChanges } from "@/src/types/purchaseTypes";
import { logger } from "@/src/utils/logger";
@ -53,8 +53,9 @@ export const handlePurchase = async (
const prePurchaseInventoryChanges: IInventoryChanges = {};
let seed: bigint | undefined;
if (purchaseRequest.PurchaseParams.Source == 7) {
const manifest = getVendorManifestByOid(purchaseRequest.PurchaseParams.SourceId!);
let manifest = getVendorManifestByOid(purchaseRequest.PurchaseParams.SourceId!);
if (manifest) {
manifest = applyStandingToVendorManifest(inventory, manifest);
let ItemId: string | undefined;
if (purchaseRequest.PurchaseParams.ExtraPurchaseInfoJson) {
ItemId = (JSON.parse(purchaseRequest.PurchaseParams.ExtraPurchaseInfoJson) as { ItemId: string })
@ -92,7 +93,7 @@ export const handlePurchase = async (
if (!config.noVendorPurchaseLimits && ItemId) {
inventory.RecentVendorPurchases ??= [];
let vendorPurchases = inventory.RecentVendorPurchases.find(
x => x.VendorType == manifest.VendorInfo.TypeName
x => x.VendorType == manifest!.VendorInfo.TypeName
);
if (!vendorPurchases) {
vendorPurchases =

View File

@ -86,54 +86,12 @@ export const mixSeeds = (seed1: number, seed2: number): number => {
return seed >>> 0;
};
// Seeded RNG for internal usage. Based on recommendations in the ISO C standards.
export class CRng {
state: number;
constructor(seed: number = 1) {
this.state = seed;
}
random(): number {
this.state = (this.state * 1103515245 + 12345) & 0x7fffffff;
return (this.state & 0x3fffffff) / 0x3fffffff;
}
randomInt(min: number, max: number): number {
const diff = max - min;
if (diff != 0) {
if (diff < 0) {
throw new Error(`max must be greater than min`);
}
if (diff > 0x3fffffff) {
throw new Error(`insufficient entropy`);
}
min += Math.floor(this.random() * (diff + 1));
}
return min;
}
randomElement<T>(arr: readonly T[]): T | undefined {
return arr[Math.floor(this.random() * arr.length)];
}
randomReward<T extends { probability: number }>(pool: T[]): T | undefined {
return getRewardAtPercentage(pool, this.random());
}
churnSeed(its: number): void {
while (its--) {
this.state = (this.state * 1103515245 + 12345) & 0x7fffffff;
}
}
}
// Seeded RNG for cases where we need identical results to the game client. Based on work by Donald Knuth.
// Seeded RNG with identical results to the game client. Based on work by Donald Knuth.
export class SRng {
state: bigint;
constructor(seed: bigint) {
this.state = seed;
constructor(seed: bigint | number) {
this.state = BigInt(seed);
}
randomInt(min: number, max: number): number {
@ -157,4 +115,19 @@ export class SRng {
randomReward<T extends { probability: number }>(pool: T[]): T | undefined {
return getRewardAtPercentage(pool, this.randomFloat());
}
churnSeed(its: number): void {
while (its--) {
this.state = (0x5851f42d4c957f2dn * this.state + 0x14057b7ef767814fn) & 0xffffffffffffffffn;
}
}
shuffleArray<T>(arr: T[]): void {
for (let lastIdx = arr.length - 1; lastIdx >= 1; --lastIdx) {
const swapIdx = this.randomInt(0, lastIdx);
const tmp = arr[swapIdx];
arr[swapIdx] = arr[lastIdx];
arr[lastIdx] = tmp;
}
}
}

View File

@ -1,9 +1,12 @@
import { unixTimesInMs } from "@/src/constants/timeConstants";
import { isDev } from "@/src/helpers/pathHelper";
import { catBreadHash } from "@/src/helpers/stringHelpers";
import { CRng, mixSeeds } from "@/src/services/rngService";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { mixSeeds, SRng } from "@/src/services/rngService";
import { IMongoDate } from "@/src/types/commonTypes";
import { IItemManifest, IVendorInfo, IVendorManifest } from "@/src/types/vendorTypes";
import { ExportVendors, IRange } from "warframe-public-export-plus";
import { logger } from "@/src/utils/logger";
import { ExportVendors, IRange, IVendor } from "warframe-public-export-plus";
import ArchimedeanVendorManifest from "@/static/fixed_responses/getVendorInfo/ArchimedeanVendorManifest.json";
import DeimosEntratiFragmentVendorProductsManifest from "@/static/fixed_responses/getVendorInfo/DeimosEntratiFragmentVendorProductsManifest.json";
@ -24,7 +27,6 @@ import MaskSalesmanManifest from "@/static/fixed_responses/getVendorInfo/MaskSal
import Nova1999ConquestShopManifest from "@/static/fixed_responses/getVendorInfo/Nova1999ConquestShopManifest.json";
import OstronPetVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronPetVendorManifest.json";
import OstronProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronProspectorVendorManifest.json";
import RadioLegionIntermission12VendorManifest from "@/static/fixed_responses/getVendorInfo/RadioLegionIntermission12VendorManifest.json";
import SolarisDebtTokenVendorRepossessionsManifest from "@/static/fixed_responses/getVendorInfo/SolarisDebtTokenVendorRepossessionsManifest.json";
import SolarisProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/SolarisProspectorVendorManifest.json";
import Temple1999VendorManifest from "@/static/fixed_responses/getVendorInfo/Temple1999VendorManifest.json";
@ -51,7 +53,6 @@ const rawVendorManifests: IVendorManifest[] = [
Nova1999ConquestShopManifest,
OstronPetVendorManifest,
OstronProspectorVendorManifest,
RadioLegionIntermission12VendorManifest,
SolarisDebtTokenVendorRepossessionsManifest,
SolarisProspectorVendorManifest,
Temple1999VendorManifest,
@ -81,12 +82,6 @@ const generatableVendors: IGeneratableVendorInfo[] = [
WeaponUpgradeValueAttenuationExponent: 2.25,
cycleOffset: 1744934400_000,
cycleDuration: 4 * unixTimesInMs.day
},
{
_id: { $oid: "61ba123467e5d37975aeeb03" },
TypeName: "/Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest",
RandomSeedType: "VRST_FLAVOUR_TEXT",
cycleDuration: unixTimesInMs.week // TODO: Auto-detect this based on the items, so we don't need to specify it explicitly.
}
// {
// _id: { $oid: "5dbb4c41e966f7886c3ce939" },
@ -98,6 +93,25 @@ const getVendorOid = (typeName: string): string => {
return "5be4a159b144f3cd" + catBreadHash(typeName).toString(16).padStart(8, "0");
};
// https://stackoverflow.com/a/17445304
const gcd = (a: number, b: number): number => {
return b ? gcd(b, a % b) : a;
};
const getCycleDuration = (manifest: IVendor): number => {
let dur = 0;
for (const item of manifest.items) {
if (typeof item.durationHours != "number") {
dur = 1;
break;
}
if (dur != item.durationHours) {
dur = gcd(dur, item.durationHours);
}
}
return dur * unixTimesInMs.hour;
};
export const getVendorManifestByTypeName = (typeName: string): IVendorManifest | undefined => {
for (const vendorManifest of rawVendorManifests) {
if (vendorManifest.VendorInfo.TypeName == typeName) {
@ -110,11 +124,12 @@ export const getVendorManifestByTypeName = (typeName: string): IVendorManifest |
}
}
if (typeName in ExportVendors) {
const manifest = ExportVendors[typeName];
return generateVendorManifest({
_id: { $oid: getVendorOid(typeName) },
TypeName: typeName,
RandomSeedType: ExportVendors[typeName].randomSeedType,
cycleDuration: unixTimesInMs.hour
RandomSeedType: manifest.randomSeedType,
cycleDuration: getCycleDuration(manifest)
});
}
return undefined;
@ -138,13 +153,50 @@ export const getVendorManifestByOid = (oid: string): IVendorManifest | undefined
_id: { $oid: typeNameOid },
TypeName: typeName,
RandomSeedType: manifest.randomSeedType,
cycleDuration: unixTimesInMs.hour
cycleDuration: getCycleDuration(manifest)
});
}
}
return undefined;
};
export const applyStandingToVendorManifest = (
inventory: TInventoryDatabaseDocument,
vendorManifest: IVendorManifest
): IVendorManifest => {
return {
VendorInfo: {
...vendorManifest.VendorInfo,
ItemManifest: [...vendorManifest.VendorInfo.ItemManifest].map(offer => {
if (offer.Affiliation && offer.ReductionPerPositiveRank && offer.IncreasePerNegativeRank) {
const title: number = inventory.Affiliations.find(x => x.Tag == offer.Affiliation)?.Title ?? 0;
const factor =
1 + (title < 0 ? offer.IncreasePerNegativeRank : offer.ReductionPerPositiveRank) * title * -1;
//console.log(offer.Affiliation, title, factor);
if (factor) {
offer = { ...offer };
if (offer.RegularPrice) {
offer.RegularPriceBeforeDiscount = offer.RegularPrice;
offer.RegularPrice = [
Math.trunc(offer.RegularPriceBeforeDiscount[0] * factor),
Math.trunc(offer.RegularPriceBeforeDiscount[1] * factor)
];
}
if (offer.ItemPrices) {
offer.ItemPricesBeforeDiscount = offer.ItemPrices;
offer.ItemPrices = [];
for (const item of offer.ItemPricesBeforeDiscount) {
offer.ItemPrices.push({ ...item, ItemCount: Math.trunc(item.ItemCount * factor) });
}
}
}
}
return offer;
})
}
};
};
const preprocessVendorManifest = (originalManifest: IVendorManifest): IVendorManifest => {
if (Date.now() >= parseInt(originalManifest.VendorInfo.Expiry.$date.$numberLong)) {
const manifest = structuredClone(originalManifest);
@ -176,24 +228,27 @@ const toRange = (value: IRange | number): IRange => {
return value;
};
const vendorInfoCache: Record<string, IVendorInfo> = {};
const vendorManifestCache: Record<string, IVendorManifest> = {};
const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorManifest => {
if (!(vendorInfo.TypeName in vendorInfoCache)) {
if (!(vendorInfo.TypeName in vendorManifestCache)) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { cycleOffset, cycleDuration, ...clientVendorInfo } = vendorInfo;
vendorInfoCache[vendorInfo.TypeName] = {
...clientVendorInfo,
ItemManifest: [],
Expiry: { $date: { $numberLong: "0" } }
vendorManifestCache[vendorInfo.TypeName] = {
VendorInfo: {
...clientVendorInfo,
ItemManifest: [],
Expiry: { $date: { $numberLong: "0" } }
}
};
}
const processed = vendorInfoCache[vendorInfo.TypeName];
if (Date.now() >= parseInt(processed.Expiry.$date.$numberLong)) {
const cacheEntry = vendorManifestCache[vendorInfo.TypeName];
const info = cacheEntry.VendorInfo;
if (Date.now() >= parseInt(info.Expiry.$date.$numberLong)) {
// Remove expired offers
for (let i = 0; i != processed.ItemManifest.length; ) {
if (Date.now() >= parseInt(processed.ItemManifest[i].Expiry.$date.$numberLong)) {
processed.ItemManifest.splice(i, 1);
for (let i = 0; i != info.ItemManifest.length; ) {
if (Date.now() >= parseInt(info.ItemManifest[i].Expiry.$date.$numberLong)) {
info.ItemManifest.splice(i, 1);
} else {
++i;
}
@ -204,15 +259,31 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
const cycleOffset = vendorInfo.cycleOffset ?? 1734307200_000;
const cycleDuration = vendorInfo.cycleDuration;
const cycleIndex = Math.trunc((Date.now() - cycleOffset) / cycleDuration);
const rng = new CRng(mixSeeds(vendorSeed, cycleIndex));
const rng = new SRng(mixSeeds(vendorSeed, cycleIndex));
const manifest = ExportVendors[vendorInfo.TypeName];
const offersToAdd = [];
if (manifest.numItems && !manifest.isOneBinPerCycle) {
if (
manifest.numItems &&
(manifest.numItems.minValue != manifest.numItems.maxValue ||
manifest.items.length != manifest.numItems.minValue) &&
!manifest.isOneBinPerCycle
) {
const remainingItemCapacity: Record<string, number> = {};
for (const item of manifest.items) {
remainingItemCapacity[item.storeItem] = 1 + item.duplicates;
}
for (const offer of info.ItemManifest) {
remainingItemCapacity[offer.StoreItem] -= 1;
}
const numItemsTarget = rng.randomInt(manifest.numItems.minValue, manifest.numItems.maxValue);
while (processed.ItemManifest.length + offersToAdd.length < numItemsTarget) {
while (info.ItemManifest.length + offersToAdd.length < numItemsTarget) {
// TODO: Consider per-bin item limits
// TODO: Consider item probability weightings
offersToAdd.push(rng.randomElement(manifest.items)!);
const item = rng.randomElement(manifest.items)!;
if (remainingItemCapacity[item.storeItem] != 0) {
remainingItemCapacity[item.storeItem] -= 1;
offersToAdd.push(item);
}
}
} else {
let binThisCycle;
@ -232,7 +303,7 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
}
const cycleStart = cycleOffset + cycleIndex * cycleDuration;
for (const rawItem of offersToAdd) {
const durationHoursRange = toRange(rawItem.durationHours);
const durationHoursRange = toRange(rawItem.durationHours ?? cycleDuration);
const expiry =
cycleStart +
rng.randomInt(durationHoursRange.minValue, durationHoursRange.maxValue) * unixTimesInMs.hour;
@ -247,8 +318,7 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
$oid:
((cycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") +
vendorInfo._id.$oid.substring(8, 16) +
rng.randomInt(0, 0xffff).toString(16).padStart(4, "0") +
rng.randomInt(0, 0xffff).toString(16).padStart(4, "0")
rng.randomInt(0, 0xffff_ffff).toString(16).padStart(8, "0")
}
};
if (rawItem.numRandomItemPrices) {
@ -283,26 +353,39 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
item.PremiumPrice = [value, value];
}
if (vendorInfo.RandomSeedType) {
item.LocTagRandSeed = (rng.randomInt(0, 0xffff) << 16) | rng.randomInt(0, 0xffff);
item.LocTagRandSeed = rng.randomInt(0, 0xffff_ffff);
if (vendorInfo.RandomSeedType == "VRST_WEAPON") {
const highDword = (rng.randomInt(0, 0xffff) << 16) | rng.randomInt(0, 0xffff);
const highDword = rng.randomInt(0, 0xffff_ffff);
item.LocTagRandSeed = (BigInt(highDword) << 32n) | (BigInt(item.LocTagRandSeed) & 0xffffffffn);
}
}
processed.ItemManifest.push(item);
info.ItemManifest.push(item);
}
// Update vendor expiry
let soonestOfferExpiry: number = Number.MAX_SAFE_INTEGER;
for (const offer of processed.ItemManifest) {
for (const offer of info.ItemManifest) {
const offerExpiry = parseInt(offer.Expiry.$date.$numberLong);
if (soonestOfferExpiry > offerExpiry) {
soonestOfferExpiry = offerExpiry;
}
}
processed.Expiry.$date.$numberLong = soonestOfferExpiry.toString();
info.Expiry.$date.$numberLong = soonestOfferExpiry.toString();
}
return {
VendorInfo: processed
};
return cacheEntry;
};
if (isDev) {
const ads = getVendorManifestByTypeName("/Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest")!
.VendorInfo.ItemManifest;
if (
ads.length != 5 ||
ads[0].Bin != "BIN_4" ||
ads[1].Bin != "BIN_3" ||
ads[2].Bin != "BIN_2" ||
ads[3].Bin != "BIN_1" ||
ads[4].Bin != "BIN_0"
) {
logger.warn(`self test failed for /Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest`);
}
}

View File

@ -0,0 +1,65 @@
import http from "http";
import https from "https";
import fs from "node:fs";
import { config } from "./configService";
import { logger } from "../utils/logger";
import { app } from "../app";
import { AddressInfo } from "node:net";
let httpServer: http.Server | undefined;
let httpsServer: https.Server | undefined;
const tlsOptions = {
key: fs.readFileSync("static/certs/key.pem"),
cert: fs.readFileSync("static/certs/cert.pem")
};
export const startWebServer = (): void => {
const httpPort = config.httpPort || 80;
const httpsPort = config.httpsPort || 443;
// eslint-disable-next-line @typescript-eslint/no-misused-promises
httpServer = http.createServer(app);
httpServer.listen(httpPort, () => {
logger.info("HTTP server started on port " + httpPort);
// eslint-disable-next-line @typescript-eslint/no-misused-promises
httpsServer = https.createServer(tlsOptions, app);
httpsServer.listen(httpsPort, () => {
logger.info("HTTPS server started on port " + httpsPort);
logger.info(
"Access the WebUI in your browser at http://localhost" + (httpPort == 80 ? "" : ":" + httpPort)
);
});
});
};
export const getWebPorts = (): Record<"http" | "https", number | undefined> => {
return {
http: (httpServer?.address() as AddressInfo | undefined)?.port,
https: (httpsServer?.address() as AddressInfo | undefined)?.port
};
};
export const stopWebServer = async (): Promise<void> => {
const promises: Promise<void>[] = [];
if (httpServer) {
promises.push(
new Promise(resolve => {
httpServer!.close(() => {
resolve();
});
})
);
}
if (httpsServer) {
promises.push(
new Promise(resolve => {
httpsServer!.close(() => {
resolve();
});
})
);
}
await Promise.all(promises);
};

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,11 @@ export interface IOid {
$oid: string;
}
export interface IOidWithLegacySupport {
$oid?: string;
$id?: string;
}
export interface IMongoDate {
$date: {
$numberLong: string;

24
src/types/friendTypes.ts Normal file
View File

@ -0,0 +1,24 @@
import { Types } from "mongoose";
import { IMongoDate, IOidWithLegacySupport } from "./commonTypes";
export interface IFriendInfo {
_id: IOidWithLegacySupport;
DisplayName?: string;
PlatformNames?: string[];
PlatformAccountId?: string;
Status?: number;
ActiveAvatarImageType?: string;
LastLogin?: IMongoDate;
PlayerLevel?: number;
Suffix?: number;
Note?: string;
Favorite?: boolean;
NewRequest?: boolean;
}
export interface IFriendship {
owner: Types.ObjectId;
friend: Types.ObjectId;
Note?: string;
Favorite?: boolean;
}

View File

@ -1,10 +1,11 @@
import { Types } from "mongoose";
import { IOid, IMongoDate } from "@/src/types/commonTypes";
import { IOid, IMongoDate, IOidWithLegacySupport } from "@/src/types/commonTypes";
import { IFusionTreasure, IMiscItem, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes";
import { IPictureFrameInfo } from "./shipTypes";
import { IFriendInfo } from "./friendTypes";
export interface IGuildClient {
_id: IOid;
_id: IOidWithLegacySupport;
Name: string;
MOTD: string;
LongMOTD?: ILongMOTD;
@ -21,7 +22,7 @@ export interface IGuildClient {
CeremonyResetDate?: IMongoDate;
CrossPlatformEnabled?: boolean;
AutoContributeFromVault?: boolean;
AllianceId?: IOid;
AllianceId?: IOidWithLegacySupport;
}
export interface IGuildDatabase {
@ -70,7 +71,6 @@ export interface ILongMOTD {
authorGuildName?: string;
}
// 32 seems to be reserved
export enum GuildPermission {
Ruler = 1, // Clan: Change hierarchy. Alliance (Creator only): Kick clans.
Advertiser = 8192,
@ -78,6 +78,7 @@ export enum GuildPermission {
Regulator = 4, // Kick members
Promoter = 8, // Clan: Promote and demote members. Alliance (Creator only): Change clan permissions.
Architect = 16, // Create and destroy rooms
Host = 32, // No longer used in modern versions
Decorator = 1024, // Create and destroy decos
Treasurer = 64, // Clan: Contribute from vault and edit tax rate. Alliance: Divvy vault.
Tech = 128, // Queue research
@ -104,21 +105,6 @@ export interface IGuildMemberDatabase {
ShipDecorationsContributed?: ITypeCount[];
}
export interface IFriendInfo {
_id: IOid;
DisplayName?: string;
PlatformNames?: string[];
PlatformAccountId?: string;
Status?: number;
ActiveAvatarImageType?: string;
LastLogin?: IMongoDate;
PlayerLevel?: number;
Suffix?: number;
Note?: string;
Favorite?: boolean;
NewRequest?: boolean;
}
// GuildMemberInfo
export interface IGuildMemberClient extends IFriendInfo {
Rank: number;
@ -141,13 +127,13 @@ export interface IGuildVault {
}
export interface IDojoClient {
_id: IOid; // ID of the guild
_id: IOidWithLegacySupport; // ID of the guild
Name: string;
Tier: number;
TradeTax?: number;
FixedContributions: boolean;
DojoRevision: number;
AllianceId?: IOid;
AllianceId?: IOidWithLegacySupport;
Vault?: IGuildVault;
Class?: number; // Level
RevisionTime: number;
@ -162,11 +148,11 @@ export interface IDojoClient {
}
export interface IDojoComponentClient {
id: IOid;
SortId?: IOid;
id: IOidWithLegacySupport;
SortId?: IOidWithLegacySupport;
pf: string; // Prefab (.level)
ppf: string;
pi?: IOid; // Parent ID. N/A to root.
pi?: IOidWithLegacySupport; // Parent ID. N/A to root.
op?: string; // Name of the door within this room that leads to its parent. N/A to root.
pp?: string; // Name of the door within the parent that leads to this room. N/A to root.
Name?: string;
@ -180,7 +166,7 @@ export interface IDojoComponentClient {
DestructionTimeRemaining?: number; // old versions
Decos?: IDojoDecoClient[];
DecoCapacity?: number;
PaintBot?: IOid;
PaintBot?: IOidWithLegacySupport;
PendingColors?: number[];
Colors?: number[];
PendingLights?: number[];
@ -205,7 +191,7 @@ export interface IDojoComponentDatabase
}
export interface IDojoDecoClient {
id: IOid;
id: IOidWithLegacySupport;
Type: string;
Pos: number[];
Rot: number[];
@ -299,7 +285,7 @@ export interface IGuildAdDatabase {
}
export interface IAllianceClient {
_id: IOid;
_id: IOidWithLegacySupport;
Name: string;
MOTD?: ILongMOTD;
LongMOTD?: ILongMOTD;
@ -320,7 +306,7 @@ export interface IAllianceDatabase {
}
export interface IAllianceMemberClient {
_id: IOid;
_id: IOidWithLegacySupport;
Name: string;
Tier: number;
Pending: boolean;
@ -328,7 +314,7 @@ export interface IAllianceMemberClient {
Permissions: number;
MemberCount: number;
ClanLeader?: string;
ClanLeaderId?: IOid;
ClanLeaderId?: IOidWithLegacySupport;
OriginalPlatform?: number;
}

View File

@ -1,4 +1,4 @@
import { IMongoDate, IOid } from "@/src/types/commonTypes";
import { IMongoDate, IOid, IOidWithLegacySupport } from "@/src/types/commonTypes";
import { Types } from "mongoose";
import {
ICrewShipCustomization,
@ -92,7 +92,7 @@ export interface IEquipmentClient
IEquipmentDatabase,
"_id" | "InfestationDate" | "Expiry" | "UpgradesExpiry" | "UmbraDate" | "CrewMembers" | "Details"
> {
ItemId: IOid;
ItemId: IOidWithLegacySupport;
InfestationDate?: IMongoDate;
Expiry?: IMongoDate;
UpgradesExpiry?: IMongoDate;

View File

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Types } from "mongoose";
import { IOid, IMongoDate } from "../commonTypes";
import { IOid, IMongoDate, IOidWithLegacySupport } from "../commonTypes";
import {
IColor,
IItemConfig,
@ -11,6 +11,8 @@ import {
IOperatorConfigDatabase
} from "@/src/types/inventoryTypes/commonInventoryTypes";
import { IFingerprintStat, RivenFingerprint } from "@/src/helpers/rivenHelper";
import { IOrbiter } from "../personalRoomsTypes";
import { ICountedStoreItem } from "warframe-public-export-plus";
export type InventoryDatabaseEquipment = {
[_ in TEquipmentKey]: IEquipmentDatabase[];
@ -53,6 +55,7 @@ export interface IInventoryDatabase
| "CrewMembers"
| "QualifyingInvasions"
| "LastInventorySync"
| "EndlessXP"
| TEquipmentKey
>,
InventoryDatabaseEquipment {
@ -91,6 +94,7 @@ export interface IInventoryDatabase
CrewMembers: ICrewMemberDatabase[];
QualifyingInvasions: IInvasionProgressDatabase[];
LastInventorySync?: Types.ObjectId;
EndlessXP?: IEndlessXpProgressDatabase[];
}
export interface IQuestKeyDatabase {
@ -292,7 +296,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
SortieRewardAttenuation?: ISortieRewardAttenuation[];
Drones: IDroneClient[];
StepSequencers: IStepSequencer[];
ActiveAvatarImageType: string;
ActiveAvatarImageType?: string;
ShipDecorations: ITypeCount[];
DiscoveredMarkers: IDiscoveredMarker[];
//CompletedJobs: ICompletedJob[];
@ -355,7 +359,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
PendingCoupon?: IPendingCouponClient;
Harvestable: boolean;
DeathSquadable: boolean;
EndlessXP?: IEndlessXpProgress[];
EndlessXP?: IEndlessXpProgressClient[];
DialogueHistory?: IDialogueHistoryClient;
CalendarProgress?: ICalendarProgress;
SongChallenges?: ISongChallenge[];
@ -370,9 +374,10 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
EchoesHexConquestCacheScoreMission?: number;
EchoesHexConquestActiveFrameVariants?: string[];
EchoesHexConquestActiveStickers?: string[];
BrandedSuits?: IOid[];
BrandedSuits?: IOidWithLegacySupport[];
LockedWeaponGroup?: ILockedWeaponGroupClient;
HubNpcCustomizations?: IHubNpcCustomization[];
Ship?: IOrbiter; // U22 and below, response only
}
export interface IAffiliation {
@ -530,6 +535,16 @@ export interface IUpgradeDatabase extends Omit<IUpgradeClient, "ItemId"> {
_id: Types.ObjectId;
}
export interface IUpgradeFromClient {
ItemType: string;
ItemId: IOidWithLegacySupport;
FromSKU?: boolean;
UpgradeFingerprint: string;
PendingRerollFingerprint: string;
ItemCount: number;
LastAdded: IOidWithLegacySupport;
}
export interface ICrewShipMembersClient {
SLOT_A?: ICrewShipMemberClient;
SLOT_B?: ICrewShipMemberClient;
@ -558,7 +573,7 @@ export interface ICrewShipCustomization {
export interface IShipExterior {
SkinFlavourItem?: string;
Colors: IColor;
Colors?: IColor;
ShipAttachments?: IShipAttachments;
}
@ -750,7 +765,8 @@ export interface IKubrowPetDetailsClient extends Omit<IKubrowPetDetailsDatabase,
export enum Status {
StatusAvailable = "STATUS_AVAILABLE",
StatusStasis = "STATUS_STASIS"
StatusStasis = "STATUS_STASIS",
StatusIncubating = "STATUS_INCUBATING"
}
export interface ILastSortieRewardClient {
@ -848,6 +864,8 @@ export interface IMission extends IMissionDatabase {
RewardsCooldownTime?: IMongoDate;
}
export type TNemesisFaction = "FC_GRINEER" | "FC_CORPUS" | "FC_INFESTATION";
export interface INemesisBaseClient {
fp: bigint | number;
manifest: string;
@ -857,7 +875,7 @@ export interface INemesisBaseClient {
WeaponIdx: number;
AgentIdx: number;
BirthNode: string;
Faction: string;
Faction: TNemesisFaction;
Rank: number;
k: boolean;
Traded: boolean;
@ -912,10 +930,14 @@ export interface IPendingRecipeDatabase {
Pistols?: IEquipmentDatabase[];
Melee?: IEquipmentDatabase[];
SuitToUnbrand?: Types.ObjectId;
KubrowPet?: Types.ObjectId;
}
export interface IPendingRecipeClient
extends Omit<IPendingRecipeDatabase, "CompletionDate" | "LongGuns" | "Pistols" | "Melee" | "SuitToUnbrand"> {
extends Omit<
IPendingRecipeDatabase,
"CompletionDate" | "LongGuns" | "Pistols" | "Melee" | "SuitToUnbrand" | "KubrowPet"
> {
CompletionDate: IMongoDate;
}
@ -1048,7 +1070,7 @@ export interface IQuestStage {
export interface IRawUpgrade {
ItemType: string;
ItemCount: number;
LastAdded?: IOid;
LastAdded?: IOidWithLegacySupport;
}
export interface ISeasonChallenge {
@ -1141,9 +1163,24 @@ export interface IEvolutionProgress {
export type TEndlessXpCategory = "EXC_NORMAL" | "EXC_HARD";
export interface IEndlessXpProgress {
export interface IEndlessXpProgressDatabase {
Category: TEndlessXpCategory;
Earn: number;
Claim: number;
BonusAvailable?: Date;
Expiry?: Date;
Choices: string[];
PendingRewards: IEndlessXpReward[];
}
export interface IEndlessXpProgressClient extends Omit<IEndlessXpProgressDatabase, "BonusAvailable" | "Expiry"> {
BonusAvailable?: IMongoDate;
Expiry?: IMongoDate;
}
export interface IEndlessXpReward {
RequiredTotalXp: number;
Rewards: ICountedStoreItem[];
}
export interface IDialogueHistoryClient {

Some files were not shown because too many files have changed in this diff Show More