Compare commits

...

567 Commits

Author SHA1 Message Date
3e2e73f6eb feat: handle Boosters in missionInventoryUpdate (#1311)
Closes #751

Reviewed-on: OpenWF/SpaceNinjaServer#1311
2025-03-24 01:38:32 -07:00
ac25ee5118 feat: redeemPromoCode (#1310)
Reviewed-on: OpenWF/SpaceNinjaServer#1310
2025-03-24 01:38:08 -07:00
0085c20e11 feat(import): additional fields (#1305)
Reviewed-on: OpenWF/SpaceNinjaServer#1305
Reviewed-by: Sainan <sainan@calamity.inc>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-03-23 13:33:26 -07:00
a77c1906bf chore: add custom getAccountInfo endpoint (#1300)
This will help the IRC server get all the information it needs for permission management in a single request.

Reviewed-on: OpenWF/SpaceNinjaServer#1300
2025-03-23 13:17:14 -07:00
e7605a2e17 fix: use IMongoDate for EntratiVaultCountResetDate in inventory response (#1308)
Reviewed-on: OpenWF/SpaceNinjaServer#1308
2025-03-23 13:09:38 -07:00
db8bff20fe fix: only roll unique rewards for peely pix booster packs (#1306)
Reviewed-on: OpenWF/SpaceNinjaServer#1306
2025-03-23 13:09:13 -07:00
19bfffaa7c fix: give helmet when acquiring a skin (#1304)
Reviewed-on: OpenWF/SpaceNinjaServer#1304
2025-03-23 13:09:02 -07:00
d0df9e3731 chore: remove unused junctionRewards.json 2025-03-23 21:05:31 +01:00
8a29f06207 chore: use inventory projection for updateTheme (#1302)
Reviewed-on: OpenWF/SpaceNinjaServer#1302
2025-03-23 09:06:28 -07:00
cf3007b744 chore: update config when admin changes their name (#1298)
Reviewed-on: OpenWF/SpaceNinjaServer#1298
2025-03-23 09:06:08 -07:00
7f5592e00c chore: improve authentication and Dropped logic (#1296)
- Dropped is now also unset by getAccountForRequest
- Improved how nonce is validated to avoid possible parser mismatch issues to smuggle a 0
- Updated ircDroppedController to perform only a single MongoDB operation

Reviewed-on: OpenWF/SpaceNinjaServer#1296
2025-03-23 09:05:47 -07:00
c3d7ae33c2 chore: do 'git stash' before hard reset
Just in case the user made local changes and then runs the bat we don't wanna have it be irrecoverably lost.
2025-03-23 16:40:48 +01:00
aa12708738 chore: make addItem return InventoryChanges directly (#1299)
Reviewed-on: OpenWF/SpaceNinjaServer#1299
2025-03-23 08:26:46 -07:00
cf125b5355 chore: prettier 2025-03-23 13:59:22 +01:00
5277f7cc37 feat(import): loc pins (#1297)
Reviewed-on: OpenWF/SpaceNinjaServer#1297
2025-03-23 05:20:48 -07:00
b5a0a2297e feat: acquisition of peely pix + free pack for first visit (#1292)
Reviewed-on: OpenWF/SpaceNinjaServer#1292
2025-03-23 05:07:15 -07:00
e0d31b8988 feat: entratiLabConquestMode.php (#1291)
Reviewed-on: OpenWF/SpaceNinjaServer#1291
2025-03-23 05:06:31 -07:00
bc6f03b7c9 feat: toggle wishlisted items (#1289)
Reviewed-on: OpenWF/SpaceNinjaServer#1289
2025-03-23 05:06:06 -07:00
5817b48db9 fix: use deleteMany for models where accountId is not unique when deleting account (#1290)
Reviewed-on: OpenWF/SpaceNinjaServer#1290
2025-03-22 18:12:59 -07:00
5a56c2e9d3 feat: ascension ceremony inbox message (#1284)
Reviewed-on: OpenWF/SpaceNinjaServer#1284
2025-03-22 17:35:52 -07:00
7414658340 fix: add missing items from codex objects list to allScans (#1282)
Reviewed-on: OpenWF/SpaceNinjaServer#1282
2025-03-22 17:35:34 -07:00
4b3b551ba7 fix: properly commit boosters to inventory (#1279)
Reviewed-on: OpenWF/SpaceNinjaServer#1279
2025-03-22 17:35:18 -07:00
b8e3be5018 chore: add IOtherDialogueInfo 2025-03-22 21:11:16 +01:00
57786bfffb fix: don't touch NemesisAbandonedRewards when spawning a lich (#1275)
Because this can contain both grineer and corpus weapons, I think we should simply defer to the client's missionInventoryUpdate request in this matter.
This still leaves open the possibility of the client crashing between spawning the lich and finishing the mission, but that's rather unlikely, I guess.

Reviewed-on: OpenWF/SpaceNinjaServer#1275
2025-03-22 07:30:29 -07:00
a0453ca61d feat: nemesis mode p (#1276)
Reviewed-on: OpenWF/SpaceNinjaServer#1276
2025-03-22 07:30:16 -07:00
16bfcc44d5 chore(webui): update to translation files (#1278)
- Update to German translation.
- Added the new (previously missing) untranslated string to the other files.

Reviewed-on: OpenWF/SpaceNinjaServer#1278
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-03-22 06:44:05 -07:00
c6a2785175 feat: clearing lich infuance (#1270)
Reviewed-on: OpenWF/SpaceNinjaServer#1270
Reviewed-by: Sainan <sainan@calamity.inc>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-03-22 06:08:00 -07:00
beb02bffb0 chore(webui): say "successfully removed" when using a negative quantity (#1271)
Reviewed-on: OpenWF/SpaceNinjaServer#1271
2025-03-22 03:37:06 -07:00
468ede680a chore(webui): update russain translations (#1273)
Reviewed-on: OpenWF/SpaceNinjaServer#1273
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-03-22 03:36:55 -07:00
42aca103ed feat: handle ShipDecorations in missionInventoryUpdate (#1267)
Reviewed-on: OpenWF/SpaceNinjaServer#1267
2025-03-22 01:15:09 -07:00
aa95074ee0 chore(webui): give feedback via toasts instead of alerts (#1269)
Reviewed-on: OpenWF/SpaceNinjaServer#1269
2025-03-22 01:10:41 -07:00
5038095c13 fix(webui): hide unapplicable server settings elements (#1266)
Reviewed-on: OpenWF/SpaceNinjaServer#1266
2025-03-21 05:20:01 -07:00
3b16ff9b54 feat: getProfileViewingData for players (#1258)
Reviewed-on: OpenWF/SpaceNinjaServer#1258
2025-03-21 05:19:53 -07:00
6598318fc5 feat: daily tribute (#1241)
Closes #367

Reviewed-on: OpenWF/SpaceNinjaServer#1241
2025-03-21 05:19:42 -07:00
e83970d326 chore(stats): fix eslint warnings (#1262)
Reviewed-on: OpenWF/SpaceNinjaServer#1262
2025-03-21 04:02:49 -07:00
3c87dd56ca feat(stats): Ollie's Crash Course Rewards (#1260)
Reviewed-on: OpenWF/SpaceNinjaServer#1260
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-03-21 03:40:20 -07:00
7d3f2e8796 feat(stats): minigame stats (#1249)
Reviewed-on: OpenWF/SpaceNinjaServer#1249
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-03-21 02:40:04 -07:00
4cd35ef4d9 fix(webui): can't acquire entrati lanthorn 2025-03-21 00:48:58 +01:00
9b16dc2c6a feat: valence fusion (#1251)
Reviewed-on: OpenWF/SpaceNinjaServer#1251
2025-03-20 15:27:37 -07:00
9d90a3ca26 fix: handle creation of infested lich (#1252)
just setting the höllvania nodes and preventing the generation of a weapon index

Reviewed-on: OpenWF/SpaceNinjaServer#1252
2025-03-20 15:27:15 -07:00
b761ff1bff fix: tell client of PrimeTokens inventory change when buying from varzia (#1243)
Reviewed-on: OpenWF/SpaceNinjaServer#1243
2025-03-20 10:08:00 -07:00
31ad97e215 feat: valence swap (#1244)
Reviewed-on: OpenWF/SpaceNinjaServer#1244
2025-03-20 09:50:33 -07:00
9150d036d7 feat: installation of valence adapter (#1240)
Reviewed-on: OpenWF/SpaceNinjaServer#1240
2025-03-20 09:50:22 -07:00
1b4aee0b90 chore(webui): update to German translation (#1242)
Translated the new `cheats_intrinsicsUnlockAll` string.

Reviewed-on: OpenWF/SpaceNinjaServer#1242
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-03-20 07:23:16 -07:00
88c5999d07 chore: use SubdocumentArray.id in upgradesController (#1238)
Reviewed-on: OpenWF/SpaceNinjaServer#1238
2025-03-20 05:50:48 -07:00
f0ebeab74e fix: when acquiring lich weapon, add innate damage (#1237)
just fully randomised right now but better than adding these in a 'broken' state

Reviewed-on: OpenWF/SpaceNinjaServer#1237
2025-03-20 05:37:53 -07:00
352c6df339 fix: default PlacedDecos in schema to [] to match the type (#1235)
Reviewed-on: OpenWF/SpaceNinjaServer#1235
2025-03-20 05:36:45 -07:00
6135fdcdb9 fix: remove credits & ducats for purchases from baro (#1232)
Reviewed-on: OpenWF/SpaceNinjaServer#1232
2025-03-20 05:36:36 -07:00
2334e76453 feat(webui): max rank all intrinsics (#1230)
Reviewed-on: OpenWF/SpaceNinjaServer#1230
2025-03-20 05:36:29 -07:00
3986dac8ef fix: buying flawed mods on iron wake doesn't consume credits (#1228)
Reviewed-on: OpenWF/SpaceNinjaServer#1228
2025-03-20 05:36:17 -07:00
0e1973e246 feat: start nemesis (#1227)
Closes #446

As discussed there, some support for 64-bit integers without precision loss had to be hacked in.

Reviewed-on: OpenWF/SpaceNinjaServer#1227
2025-03-20 05:36:09 -07:00
ae05172ad8 chore: update PE+ for 38.5.0 2025-03-20 01:09:39 +01:00
6eebf0aa84 chore: update request handling for 38.5.0 2025-03-19 20:38:14 +01:00
c98d872d52 chore: use projection for drones request when possible (#1231)
Reviewed-on: OpenWF/SpaceNinjaServer#1231
2025-03-18 04:24:22 -07:00
2a703de0cb chore: replace instances of new Date().getTime() with Date.now() (#1229)
Reviewed-on: OpenWF/SpaceNinjaServer#1229
2025-03-18 04:24:11 -07:00
8728cf3abf fix(webui): add riven placeholder text 2025-03-18 10:00:12 +01:00
3e460c5728 chore: update RewardSeed in database after generating a new one (#1226)
Reviewed-on: OpenWF/SpaceNinjaServer#1226
2025-03-18 01:45:16 -07:00
f78616980a feat: archon hunt rotation (#1220)
Reviewed-on: OpenWF/SpaceNinjaServer#1220
2025-03-18 01:45:08 -07:00
b4da457501 feat: mastery rank up inbox message (#1206)
Closes #1203

Reviewed-on: OpenWF/SpaceNinjaServer#1206
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-03-17 12:23:17 -07:00
f2afa6bb55 chore: add GuildAdvertisementVendorManifest (#1221)
Reviewed-on: OpenWF/SpaceNinjaServer#1221
2025-03-17 10:43:59 -07:00
7b866a2f71 fix(webui): unable to add relics (#1222)
Reviewed-on: OpenWF/SpaceNinjaServer#1222
2025-03-17 10:06:25 -07:00
3eb5c366df fix(webui): ignore empty archon shard slots 2025-03-17 18:05:27 +01:00
6f3f1fe5b9 fix: syndicate mission oids being longer than 24 chars 2025-03-17 14:33:47 +01:00
0be54dd7ce feat: purchase modular weapon from daily special (#1217)
Closes #685

Reviewed-on: OpenWF/SpaceNinjaServer#1217
2025-03-17 05:10:44 -07:00
1d091e3c4c chore: remove consumables, recipes, etc. from array when their ItemCount becomes 0 (#1216)
Reviewed-on: OpenWF/SpaceNinjaServer#1216
2025-03-17 05:10:28 -07:00
6d12d90877 chore: add indexes for various models (#1213)
These are looked up by the owner account id and/or assumed to exist only once per account.
No index was added for "Ships" as that does not match these critera.

Reviewed-on: OpenWF/SpaceNinjaServer#1213
2025-03-16 08:46:02 -07:00
943edf7065 chore: use updateOne for active focus way change 2025-03-16 16:41:39 +01:00
1d23f2736f chore: use inventory projection for getGuild requests (#1212)
Reviewed-on: OpenWF/SpaceNinjaServer#1212
2025-03-16 08:16:49 -07:00
05356af9bd chore: use updateOne for simple inventory field setters (#1211)
e.g. changing syndicate pledge now takes ~6 ms instead of ~84 ms.

Reviewed-on: OpenWF/SpaceNinjaServer#1211
2025-03-16 08:16:27 -07:00
818e09d4af fix: only track clan log dateTime once contributions are done (#1210)
Reviewed-on: OpenWF/SpaceNinjaServer#1210
2025-03-16 08:16:11 -07:00
c3a9b42fa2 fix: update slots where addEquipment is used (#1207)
Reviewed-on: OpenWF/SpaceNinjaServer#1207
2025-03-16 04:33:48 -07:00
651ab5f6f1 feat: death marks (#1205)
Reviewed-on: OpenWF/SpaceNinjaServer#1205
2025-03-16 04:33:21 -07:00
b7f05e851c fix: handle high spoofed mastery rank plus noDailyStandingLimits (#1201)
In case the spoofed mastery rank is so high that 999,999 is *less* than the assumed maximum value for daily affiliation bins, we'll just use that so that the bar is always (at least) 100% full.

Reviewed-on: OpenWF/SpaceNinjaServer#1201
2025-03-16 04:33:12 -07:00
ecc2e35535 feat: randomly generate daily modular weapon sales (#1199)
Re #685

Reviewed-on: OpenWF/SpaceNinjaServer#1199
2025-03-16 04:32:57 -07:00
ab11f67f0b feat: clan polychrome research (#1177)
Reviewed-on: OpenWF/SpaceNinjaServer#1177
2025-03-16 04:32:11 -07:00
56a372ee6f chore(webui): update to German translation (#1204)
Reviewed-on: OpenWF/SpaceNinjaServer#1204
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-03-15 12:44:46 -07:00
56fecef1bf chore: set HasOwnedVoidProjectionsPreviously when acquiring a relic (#1198)
Reviewed-on: OpenWF/SpaceNinjaServer#1198
2025-03-15 10:25:32 -07:00
adddc11b6f fix: limit booster pack purchases to a max quantity of 100 (#1189)
Reviewed-on: OpenWF/SpaceNinjaServer#1189
2025-03-15 10:25:15 -07:00
2d6e096fde feat: argon crystal decay (#1195)
Reviewed-on: OpenWF/SpaceNinjaServer#1195
2025-03-15 06:39:54 -07:00
2f59b3d775 chore(webui): update to German and Chinese translation file (#1196)
- Translated the latest new vendor string into German in `de.js`
- Added the `[UNTRANSLATED] No Vendor Purchase Limits` placeholder string in `zh.js` because that file was missed.

Reviewed-on: OpenWF/SpaceNinjaServer#1196
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-03-15 06:33:21 -07:00
294bedd29a fix(stats): add captures to stat model (#1191)
Reviewed-on: OpenWF/SpaceNinjaServer#1191
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-03-15 03:48:47 -07:00
ae9a98ca8b fix(stats): handle eidolon capture (#1190)
Reviewed-on: OpenWF/SpaceNinjaServer#1190
Reviewed-by: Sainan <sainan@calamity.inc>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-03-15 03:25:02 -07:00
2891e2fef5 chore: fix various eslint issues (#1176)
Reviewed-on: OpenWF/SpaceNinjaServer#1176
2025-03-15 03:24:39 -07:00
db20369eb9 feat: add config options for event boosters (#1184)
Reviewed-on: OpenWF/SpaceNinjaServer#1184
2025-03-15 03:21:54 -07:00
25dfbf4724 feat: edit clan tax rate (#1183)
Reviewed-on: OpenWF/SpaceNinjaServer#1183
2025-03-15 03:21:40 -07:00
114e175efb chore: set HWIDProtectEnabled so trading post can be used (#1182)
Reviewed-on: OpenWF/SpaceNinjaServer#1182
2025-03-15 03:21:26 -07:00
0facdd1af9 chore: check permissions for various clan requests (#1175)
Reviewed-on: OpenWF/SpaceNinjaServer#1175
2025-03-14 07:09:28 -07:00
236cccc137 fix: remove dojo key after being kicked from clan 2025-03-14 11:21:56 +01:00
0c06776985 feat: track RoomChanges in clan log (#1174)
Final part for clan log; closes #1152

Reviewed-on: OpenWF/SpaceNinjaServer#1174
2025-03-14 02:07:08 -07:00
6508d16190 feat: track RosterActivity in clan log (#1173)
Reviewed-on: OpenWF/SpaceNinjaServer#1173
2025-03-13 10:46:08 -07:00
3a995ef6d1 chore: specify minimum typescript version required to compile 2025-03-13 13:26:24 +01:00
de4fe0311c feat: trade in modular weapons for standing (#1172)
Closes #1055

Reviewed-on: OpenWF/SpaceNinjaServer#1172
2025-03-13 05:25:46 -07:00
292ac9d41b fix: deduct 5000 credits for crafting a zaw (#1168)
Reviewed-on: OpenWF/SpaceNinjaServer#1168
2025-03-13 04:26:06 -07:00
a029c288b7 fix: free slot when selling or otherwise getting rid of items (#1169)
Reviewed-on: OpenWF/SpaceNinjaServer#1169
2025-03-13 04:25:59 -07:00
6490fadcae feat: track vendor purchases (#1153)
Closes #739

Also adds the `noVendorPurchaseLimits` cheat to disable the logic, which is enabled by default due to lack of vendor rotations.

Reviewed-on: OpenWF/SpaceNinjaServer#1153
2025-03-13 02:14:53 -07:00
b7800b6d20 feat: edit clan hierarchy (#1164)
Re #1144

Reviewed-on: OpenWF/SpaceNinjaServer#1164
2025-03-13 02:14:29 -07:00
516df61633 chore: add "project status" section to readme (#1166)
Explicitly pointing out the issue tracking should give people a better idea of the project and allow them to set expectations accordingly.

Reviewed-on: OpenWF/SpaceNinjaServer#1166
2025-03-13 02:14:21 -07:00
2ad95aecb6 chore: npm update (#1162)
The package-lock.json is smaller now, that seems good.

Reviewed-on: OpenWF/SpaceNinjaServer#1162
2025-03-12 07:59:35 -07:00
5ca72d75e2 feat: promote & demote clan members (#1163)
Re #1144

Reviewed-on: OpenWF/SpaceNinjaServer#1163
2025-03-12 07:59:29 -07:00
073eddc050 feat: track TechChanges in clan log (#1160)
Re #1152

Reviewed-on: OpenWF/SpaceNinjaServer#1160
2025-03-12 07:59:20 -07:00
42799fee7b fix(webui): Chinese translation of Traumatic Peculiar mod name (#1161)
Reviewed-on: OpenWF/SpaceNinjaServer#1161
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-03-12 06:31:14 -07:00
02ce0f57a6 chore: faithful response to getGuild & getGuildLog when not in a clan (#1159)
Reviewed-on: OpenWF/SpaceNinjaServer#1159
2025-03-12 05:10:26 -07:00
8daf0c9eda fix(webui): add mods regression 2025-03-12 12:41:45 +01:00
be6e5ce250 feat: track ClassChanges in clan log (#1157)
Re #1152

Reviewed-on: OpenWF/SpaceNinjaServer#1157
2025-03-12 01:08:15 -07:00
7acb54922f fix: occupy a sentinel slot for sentinel weapons (#1156)
Fixes #1155

Reviewed-on: OpenWF/SpaceNinjaServer#1156
2025-03-11 13:00:12 -07:00
4e0494f15d fix: ignore purchaseQuantity when getting slots via a bundle (#1151)
Fixes #1149

Reviewed-on: OpenWF/SpaceNinjaServer#1151
2025-03-11 10:32:44 -07:00
d24aac2ab2 feat: clan name discriminators (#1147)
Closes #1145

Reviewed-on: OpenWF/SpaceNinjaServer#1147
2025-03-11 10:31:56 -07:00
1b54bcd1e0 feat(webui): Chinese translation (#1154)
Co-authored-by: Belenus <hello.belenus@proton.me>
Reviewed-on: OpenWF/SpaceNinjaServer#1154
2025-03-11 17:14:33 +01:00
38dfe14776 feat: fabricate research (#1150)
Closes #910

Reviewed-on: OpenWF/SpaceNinjaServer#1150
2025-03-11 07:56:18 -07:00
ead7b67efc chore: remove ts-node dependency (#1148)
We either use ts-node-dev or compile to JS and then run that so this isn't needed.

Reviewed-on: OpenWF/SpaceNinjaServer#1148
2025-03-11 02:04:25 -07:00
fae6615df4 feat: clan members (#1143)
Now you can add/remove members and accept/decline invites.

Closes #1110

Reviewed-on: OpenWF/SpaceNinjaServer#1143
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-03-10 16:40:40 -07:00
00f6a8bd6d chore: disable cheats by default (#1139)
The config.json.example is now has all cheats/time-savers disabled so it's as faithful as possible.

Reviewed-on: OpenWF/SpaceNinjaServer#1139
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-03-10 16:22:38 -07:00
b553097fe4 fix: handle quest completion via missionInventoryUpdate (#1140)
Partial fix for #1126

Reviewed-on: OpenWF/SpaceNinjaServer#1140
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-03-10 16:22:02 -07:00
29275fcfdd feat(WebUI): German translation (#1142) 2025-03-10 20:03:05 +01:00
b0b52ccabe chore: update package-lock.json 2025-03-10 10:28:52 +01:00
cadb6bc97b fix: logic error 2025-03-10 09:15:11 +01:00
4937cf7f59 fix: handle refresh request for a single dojo component (#1136)
Reviewed-on: OpenWF/SpaceNinjaServer#1136
2025-03-09 11:16:17 -07:00
758135d19b feat(webui): add resource drones & their blueprints via "add items" (#1137)
Closes #1133

Reviewed-on: OpenWF/SpaceNinjaServer#1137
2025-03-09 11:15:45 -07:00
1ae1cf5170 fix: can't rush dojo components with infinitePlatinum (#1138)
Reviewed-on: OpenWF/SpaceNinjaServer#1138
2025-03-09 11:15:33 -07:00
0ffa9c6bc4 feat: clan motd (#1134)
Reviewed-on: OpenWF/SpaceNinjaServer#1134
2025-03-09 07:47:32 -07:00
814f4cfdad fix: consume resources & standing required for gilding (#1132)
Fixes #1122

Reviewed-on: OpenWF/SpaceNinjaServer#1132
2025-03-09 07:47:24 -07:00
d5feec2c37 chore: track inventory changes when cracking relic via addMissionRewards (#1131)
Closes #1120

Reviewed-on: OpenWF/SpaceNinjaServer#1131
2025-03-09 07:43:30 -07:00
1c276ce133 feat: stripped rewards (#1123)
Closes #683

Reviewed-on: OpenWF/SpaceNinjaServer#1123
2025-03-09 07:42:55 -07:00
6b35408144 chore: don't install dev dependencies for basic usage (#1135)
Reviewed-on: OpenWF/SpaceNinjaServer#1135
2025-03-09 07:41:24 -07:00
f6513420be feat: login conflict (#1127)
Closes #1076

Reviewed-on: OpenWF/SpaceNinjaServer#1127
2025-03-09 07:40:37 -07:00
3da02385f9 chore: auto-detect 'my address', only use config as fallback (#1125)
This is useful for LAN usage where we can use localhost on our own machine but have to use 192.168.x.y on other devices.

Reviewed-on: OpenWF/SpaceNinjaServer#1125
2025-03-09 05:45:11 -07:00
3af15881f5 fix: failure to remove shard installed via webui (#1129)
Fixes #1128

Reviewed-on: OpenWF/SpaceNinjaServer#1129
2025-03-09 03:41:12 -07:00
92d53e1c00 chore: improve addMissionInventoryUpdates (#1121)
Closes #1119

Reviewed-on: OpenWF/SpaceNinjaServer#1121
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-03-08 06:29:05 -08:00
3853fda60d feat: track NemesisAbandonedRewards (#1118)
Re #446

Reviewed-on: OpenWF/SpaceNinjaServer#1118
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-03-08 05:36:06 -08:00
901263ada3 feat: transmutation (#1112)
Closes #1098

Reviewed-on: OpenWF/SpaceNinjaServer#1112
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-03-08 04:34:41 -08:00
d7e3f33ecf feat: add custom getName endpoint (#1108)
This can be useful for an IRC server to validate the accountId & nonce given and ensure the nickname matches.

Reviewed-on: OpenWF/SpaceNinjaServer#1108
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-03-08 04:34:14 -08:00
7fdb37f2e8 fix: donate platinum from clan vault (#1107)
Reviewed-on: OpenWF/SpaceNinjaServer#1107
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-03-08 04:34:00 -08:00
457663f14a fix: claim recipe response (#1106)
Fixes #1105

The client already 'knows' the ItemCount was decremented so when we also say it in the response, it actually ends up causing the client to think the recipe was used twice.

Reviewed-on: OpenWF/SpaceNinjaServer#1106
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-03-08 04:33:45 -08:00
ec1f504bae chore(webui): allow negative quantity for "add items" & "add mods" (#1113)
Closes #1111

Reviewed-on: OpenWF/SpaceNinjaServer#1113
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-03-08 04:33:33 -08:00
6142b8d2dc feat: config option for star days event (#1104)
Reviewed-on: OpenWF/SpaceNinjaServer#1104
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-03-08 04:28:05 -08:00
537fe5dcd1 fix: ensure exalted weapons are given from giveStartingGear (#1092)
Fixes #1020

Reviewed-on: OpenWF/SpaceNinjaServer#1092
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-03-08 04:27:11 -08:00
5a843dfe53 fix: icon for welcome message (#1115)
Closes #1114

Reviewed-on: OpenWF/SpaceNinjaServer#1115
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-03-08 04:26:24 -08:00
6c7e8e908e fix: set ArchwingEnabled to true when obtaining an archwing (#1091)
Closes #984

Reviewed-on: OpenWF/SpaceNinjaServer#1091
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-03-08 03:36:52 -08:00
9acad90b12 chore: add Invasions to worldState (#1102)
Re #1097

Reviewed-on: OpenWF/SpaceNinjaServer#1102
2025-03-08 01:44:54 -08:00
f7c2c74437 feat: clan xp (#1100)
Closes #690

Reviewed-on: OpenWF/SpaceNinjaServer#1100
2025-03-08 01:44:30 -08:00
137213520e chore: npm run prettier 2025-03-07 20:16:15 +01:00
1ad26db331 fix(webui): show message when max rank all warframes has nothing to do 2025-03-07 15:19:39 +01:00
e4a3b13160 chore: simplify config (#1090)
Reviewed-on: OpenWF/SpaceNinjaServer#1090
2025-03-07 00:41:36 -08:00
59fd816b0c feat: handle EmailItems received during mission (#1088)
Closes #1087

Reviewed-on: OpenWF/SpaceNinjaServer#1088
2025-03-07 00:41:18 -08:00
530713ce5c chore: enable "incremental" in tsconfig (#1082)
This should make subsequent runs of `npm run build` a bit faster.

Reviewed-on: OpenWF/SpaceNinjaServer#1082
2025-03-07 00:40:52 -08:00
b8b1c5e008 feat: library personal target progress (#1083)
Closes #1081

Reviewed-on: OpenWF/SpaceNinjaServer#1083
2025-03-07 00:40:22 -08:00
57b3a5b9b3 feat: clan vault (#1093)
Closes #1080

Reviewed-on: OpenWF/SpaceNinjaServer#1093
2025-03-06 21:24:25 -08:00
95dedaf976 chore(webui): update Russian translation (#1096)
Reviewed-on: OpenWF/SpaceNinjaServer#1096
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-03-06 20:43:09 -08:00
519cb26044 fix(webui): remove unnecessary elements when changing language (#1095)
Fixes #1094

Reviewed-on: OpenWF/SpaceNinjaServer#1095
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-03-06 20:42:30 -08:00
77aa1caa8f feat: dojo room destruction stage (#1089)
Closes #1074

Based on what I could find, apparently only rooms need 2 hours to destroy and decos are removed instantly.

Reviewed-on: OpenWF/SpaceNinjaServer#1089
2025-03-06 07:19:01 -08:00
6daa8ab5da chore(webui): fixup french translation 2025-03-06 09:29:57 +01:00
c4ab496aa3 feat: dojo decorations (#1079)
Closes #525

Reviewed-on: OpenWF/SpaceNinjaServer#1079
2025-03-05 23:54:47 -08:00
97b61b51b7 feat(webui): french translation (#1085)
Reviewed-on: OpenWF/SpaceNinjaServer#1085
Co-authored-by: Vitruvio <vitruvio@noreply.localhost>
Co-committed-by: Vitruvio <vitruvio@noreply.localhost>
2025-03-05 22:26:00 -08:00
0de0416ba3 chore: remove unused strings 2025-03-05 16:31:13 +01:00
0a2b2f5218 chore: update URL to avoid needing a redirect (#1084)
Because it obviously doesn't bring you anywhere currently and it could be used by a third party unfortunately.

Reviewed-on: OpenWF/SpaceNinjaServer#1084
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-03-05 07:30:19 -08:00
bafc6322c2 fix: proper response for fusionTreasures.php (#1078)
Fixes #1077

Reviewed-on: OpenWF/SpaceNinjaServer#1078
2025-03-05 03:51:48 -08:00
0869bbfb27 feat: rush dojo component (#1075)
Closes #1072

This whole system is a bit weird to me. It seems the RushPlatinum is not used by the client at all, so the server just adjusts the CompletionTime. We seem to be about 1% off, but I'm not quite sure why.

Reviewed-on: OpenWF/SpaceNinjaServer#1075
2025-03-04 10:33:38 -08:00
fba1808b07 fix: failure to create a new dojo 2025-03-04 19:31:23 +01:00
2ec110733f note 2025-03-04 08:34:42 +01:00
f3f1bfc890 chore: simplify rngService (#1073)
getRandomWeightedReward now takes any object with lowercase 'rarity', and the only alternative to it is the 'uc' variant which takes any object with uppercase 'Rarity'
usage of IRngResult is now also optional

Reviewed-on: OpenWF/SpaceNinjaServer#1073
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-03-03 12:48:46 -08:00
67a275a009 feat: dojo component "collecting materials" stage (#1071)
Closes #1051

Reviewed-on: OpenWF/SpaceNinjaServer#1071
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-03-03 12:48:39 -08:00
77cadc732c chore: give baro his entire stock (from TennoCon 2024) (#1067)
given that his offers are currently not rotating

Reviewed-on: OpenWF/SpaceNinjaServer#1067
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-03-03 12:48:24 -08:00
f97bdea447 fix: send heart of deimos email when quest is given (#1065)
Fixes #1061

Reviewed-on: OpenWF/SpaceNinjaServer#1065
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-03-03 12:48:11 -08:00
3442f15c6d fix(starter-bat): don't start if build step has failed (#1069)
Reviewed-on: OpenWF/SpaceNinjaServer#1069
2025-03-03 05:48:54 -08:00
b3003b9fb3 feat: resource extractor drones (#1068)
Closes #793

Reviewed-on: OpenWF/SpaceNinjaServer#1068
2025-03-03 05:48:46 -08:00
36d12e08c7 chore: turn guild DojoComponents into a DocumentArray (#1070)
and use .id for setDojoComponentMessage

Reviewed-on: OpenWF/SpaceNinjaServer#1070
2025-03-03 05:46:16 -08:00
d7ec259e2d chore: fix inventorySchema transform for projection 2025-03-02 16:09:18 +01:00
0798d8c6b4 chore: update cert (#1056)
The *.p2ptls.com cert expires on April 16, so I'm changing it for *.viatls.com which expires on August 3.

Reviewed-on: OpenWF/SpaceNinjaServer#1056
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-03-02 04:22:20 -08:00
8a6f36a9b0 feat(webui): add relics via "add items" (#1066)
Closes #1062

Reviewed-on: OpenWF/SpaceNinjaServer#1066
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-03-02 04:21:59 -08:00
9158209059 feat: handle acquisition of EmailItems (#1064)
Fixes #1063

Reviewed-on: OpenWF/SpaceNinjaServer#1064
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-03-02 04:18:59 -08:00
70cd088ffa chore: exclude markdown files from prettier 2025-03-02 12:04:22 +01:00
bbc40d5534 feat: updateSongChallenge (#1024)
Closes #707

untested but should be correct based on all the information I could find

Reviewed-on: OpenWF/SpaceNinjaServer#1024
2025-02-28 18:18:33 -08:00
da2b50d537 feat: collectible series (#1025)
Closes #712

a bit unsure about the inbox messages, but otherwise it should be working

Reviewed-on: OpenWF/SpaceNinjaServer#1025
2025-02-28 18:09:37 -08:00
32cc8dc61b fix: preinstall potatoes on non-crafted equipment (#1037)
Fixes #1028

Reviewed-on: OpenWF/SpaceNinjaServer#1037
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-28 12:39:51 -08:00
c971a484ef feat: updateAlignment (#1039)
Closes #1038

may need some more information about how this endpoint works. had to make a few assumptions.

Reviewed-on: OpenWF/SpaceNinjaServer#1039
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-28 12:36:49 -08:00
8c662fa1ec feat: derelict vault rewards (#1049)
Closes #1045

Reviewed-on: OpenWF/SpaceNinjaServer#1049
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-28 12:36:01 -08:00
4205364bd8 feat: noDojoResearchCosts & noDojoResearchTime (#1053)
Closes #1052

Reviewed-on: OpenWF/SpaceNinjaServer#1053
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-28 12:35:14 -08:00
caec5a6cbf feat: recipes that consume weapons (#1032)
Closes #720

Reviewed-on: OpenWF/SpaceNinjaServer#1032
2025-02-28 06:47:34 -08:00
79147786f6 chore: handle a FusionTreasures entry being 0 or less (#1050)
Closes #1043

Reviewed-on: OpenWF/SpaceNinjaServer#1050
2025-02-28 06:08:46 -08:00
cfaafc2cc3 chore: remove undefined as a possible argument when committing inventory change (#1047)
Reviewed-on: OpenWF/SpaceNinjaServer#1047
2025-02-28 03:05:32 -08:00
1468c6b1d2 chore: update PE+ to 0.5.38 (#1048)
Fixess #1041

Reviewed-on: OpenWF/SpaceNinjaServer#1048
2025-02-28 03:04:59 -08:00
28b9e35d8d chore: remove string[] from combineInventoryChanges 2025-02-28 12:04:43 +01:00
05c0a91f82 chore: make this reference more future proof 2025-02-28 07:11:54 +01:00
08a4dba80b fix: put ayatan statues in FusionTreasures instead of MiscItems (#1046)
Fixes #1044

Reviewed-on: OpenWF/SpaceNinjaServer#1046
2025-02-27 21:54:31 -08:00
d63bab1bf4 fix: logic error in addCrewShipHarness 2025-02-28 03:51:08 +01:00
526ce1529b feat: unveiling rivens by doing the challenge (#1031)
Closes #722

Reviewed-on: OpenWF/SpaceNinjaServer#1031
2025-02-27 18:01:06 -08:00
9267c9929e feat(webui): acquire flawed mods & imposters via add mods (#1040)
Closes #1033

Reviewed-on: OpenWF/SpaceNinjaServer#1040
2025-02-27 18:00:37 -08:00
a8c7eebf6d chore: note for mirror lookers 2025-02-27 10:13:29 +01:00
fac3d2f901 fix: only run docker workflow on the main repository 2025-02-27 10:01:00 +01:00
550ad360d7 Revert "remove .coderabbit.yaml"
This reverts commit 0f250c61033ba76615ddf28b67224ed75dc1876e.
2025-02-27 09:58:48 +01:00
ca55b21a2a fix: display bug when activating riven via mods console (#1034)
Reviewed-on: OpenWF/SpaceNinjaServer#1034
2025-02-26 15:42:25 -08:00
08f4137d71 fix: propagate relic reward's itemCount (#1030)
Preemptive fix for a visual bug after completing a non-endless fissure.

Reviewed-on: OpenWF/SpaceNinjaServer#1030
2025-02-26 15:42:13 -08:00
58ec63f7b9 chore: update mongoose (#1036)
Reviewed-on: OpenWF/SpaceNinjaServer#1036
2025-02-26 15:41:36 -08:00
a5c45bb646 fix: consume a slot when item is crafted instead of bought via plat (#1029)
Reviewed-on: OpenWF/SpaceNinjaServer#1029
2025-02-26 15:41:07 -08:00
4471b2d64d fix(webui): typo (#1035)
Reviewed-on: OpenWF/SpaceNinjaServer#1035
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-02-26 15:15:32 -08:00
e2ee1172ed chore: fix most eslint warnings in itemDataService 2025-02-26 12:16:31 +01:00
6a6e333011 fix: update-translations ignoring translated import_submit entry 2025-02-26 06:23:45 +01:00
9893fa957f fix: purchasing SuitBin slots (#1026)
Reviewed-on: OpenWF/SpaceNinjaServer#1026
2025-02-25 21:06:21 -08:00
de794f47ba chore: npm run prettier 2025-02-26 06:00:54 +01:00
5ce2e26683 chore: fix ISlots 2025-02-26 06:00:40 +01:00
28a36052d9 feat: daily synthesis (#1014)
Closes #386
Closes #533

Reviewed-on: OpenWF/SpaceNinjaServer#1014
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-25 17:31:52 -08:00
d7628d46e9 fix: acquisition of CrewShipWeaponSkins (#1019)
Reviewed-on: OpenWF/SpaceNinjaServer#1019
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-25 17:31:33 -08:00
2b8da4af60 fix: increment LoreFragmentScans Progress when already present (#1022)
Fixes #1021

Reviewed-on: OpenWF/SpaceNinjaServer#1022
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-25 17:31:24 -08:00
8fea608b76 fix: fill upgrades array with empty strings (#1023)
Otherwise the client will "LogBug: (Invalid UpgradeId)" and may crash/raise an interrupt

Reviewed-on: OpenWF/SpaceNinjaServer#1023
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-25 17:31:16 -08:00
c13cf70814 fix: update vor's prize completion rewards 2025-02-26 02:26:22 +01:00
3945359e7d chore: simplify conversion of missionReward from PE+ (#1018)
Reviewed-on: OpenWF/SpaceNinjaServer#1018
2025-02-25 16:58:07 -08:00
a27f1c5e01 fix: converting storeitems in missionRewards (#1017)
Fixes the acquisition of blueprints as rewards, such as those rewarded by the Junctions.

Reviewed-on: OpenWF/SpaceNinjaServer#1017
Co-authored-by: VampireKitten <dynamightkobold@gmail.com>
Co-committed-by: VampireKitten <dynamightkobold@gmail.com>
2025-02-25 10:08:27 -08:00
93afc2645c fix: items from enemy caches not showing "identified" (#1016)
Reviewed-on: OpenWF/SpaceNinjaServer#1016
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-25 04:42:49 -08:00
b5b088249c fix: ignore empty mission tag in missionInventoryUpdate (#1015)
Fixes #1013

Reviewed-on: OpenWF/SpaceNinjaServer#1015
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-25 04:41:45 -08:00
e6ec144f1f feat: handle defaultUpgrades for moas and hounds (#1012)
Closes #997

Reviewed-on: OpenWF/SpaceNinjaServer#1012
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-25 04:41:14 -08:00
3d82fee99e feat: give additionalItems for weapons (#1011)
Closes #1002

Reviewed-on: OpenWF/SpaceNinjaServer#1011
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-25 04:39:59 -08:00
39f0f7de9a feat: cracking relics in non-endless missions (#1010)
Closes #415

Reviewed-on: OpenWF/SpaceNinjaServer#1010
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-25 04:38:47 -08:00
f672f05db9 fix: handle bundles being given to addItems (#1005)
This is needed for the Hex noggles email attachment

Reviewed-on: OpenWF/SpaceNinjaServer#1005
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-25 04:38:17 -08:00
4d9e6a35ab fix: use correct reward manifest for arbitrations (#1004)
Closes #939

Reviewed-on: OpenWF/SpaceNinjaServer#1004
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-25 04:36:10 -08:00
c29bf6aab5 chore: use PE+ for boosters (#1009)
Reviewed-on: OpenWF/SpaceNinjaServer#1009
2025-02-24 21:46:30 -08:00
bc07978846 chore: use creditBundles map from PE+ (#1008)
Reviewed-on: OpenWF/SpaceNinjaServer#1008
2025-02-24 21:46:20 -08:00
2efe0df2f2 chore: fix some eslint warnings (#1007)
Reviewed-on: OpenWF/SpaceNinjaServer#1007
2025-02-24 20:56:34 -08:00
38b255d41a chore: promote no-case-declarations lint to an error (#1006)
Reviewed-on: OpenWF/SpaceNinjaServer#1006
2025-02-24 20:56:27 -08:00
421164986a fix: don't throw an error if questKey already exists (#1003)
Reviewed-on: OpenWF/SpaceNinjaServer#1003
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-02-24 15:59:57 -08:00
045d933458 fix: complete junction data and crash in vors prize mission four (#1001)
Reviewed-on: OpenWF/SpaceNinjaServer#1001
Co-authored-by: OrdisPrime <134585663+OrdisPrime@users.noreply.github.com>
Co-committed-by: OrdisPrime <134585663+OrdisPrime@users.noreply.github.com>
2025-02-24 08:50:07 -08:00
9de57668ab fix: ensure quest progress exists (#1000)
Reviewed-on: OpenWF/SpaceNinjaServer#1000
Co-authored-by: OrdisPrime <134585663+OrdisPrime@users.noreply.github.com>
Co-committed-by: OrdisPrime <134585663+OrdisPrime@users.noreply.github.com>
2025-02-24 06:14:47 -08:00
ebb28d56d5 feat: acquisition of resource extractor drones (#998)
Related to #793

Reviewed-on: OpenWF/SpaceNinjaServer#998
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-24 05:28:43 -08:00
d69cba6bef chore: reuse inventory in claimCompletedRecipeController (#999)
Reviewed-on: OpenWF/SpaceNinjaServer#999
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-24 05:27:50 -08:00
50d687e59a fix: re-enable giving ship features and mission rewards from Vors Prize after skipTutorial (#996)
Reviewed-on: OpenWF/SpaceNinjaServer#996
Co-authored-by: OrdisPrime <134585663+OrdisPrime@users.noreply.github.com>
Co-committed-by: OrdisPrime <134585663+OrdisPrime@users.noreply.github.com>
2025-02-23 12:22:54 -08:00
1274304647 chore: fix type not matching reality 2025-02-23 14:10:10 +01:00
837e041db8 feat: unveil riven with cipher (#992)
Related to #722

Reviewed-on: OpenWF/SpaceNinjaServer#992
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-23 03:55:15 -08:00
84d7b5a62e fix: handle droptable giving a 3-day booster (#993)
e.g. sorties can rarely give these

Reviewed-on: OpenWF/SpaceNinjaServer#993
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-23 03:54:46 -08:00
3c2d194302 chore: replace fusionBundles map with ExportFusionBundles (#994)
Reviewed-on: OpenWF/SpaceNinjaServer#994
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-23 03:54:26 -08:00
e1af6bd598 feat: implement CreditBundle purchases (#989)
This fixes purchasing one of the few bundles that include these credit bundles. Ex: Essential Damage mod bundles

Reviewed-on: OpenWF/SpaceNinjaServer#989
Co-authored-by: nrbdev <itzneonrb@gmail.com>
Co-committed-by: nrbdev <itzneonrb@gmail.com>
2025-02-23 03:53:56 -08:00
0142aa72a8 chore: sort api post routes 2025-02-23 05:00:41 +01:00
02c0c1c2f2 chore: fix imports in api.ts 2025-02-23 04:33:28 +01:00
bf7fd42198 feat: tutorial and natural new player experience (#983)
Reviewed-on: OpenWF/SpaceNinjaServer#983
Co-authored-by: Ordis <134585663+OrdisPrime@users.noreply.github.com>
Co-committed-by: Ordis <134585663+OrdisPrime@users.noreply.github.com>
2025-02-22 11:10:52 -08:00
9203e0bf4d feat: infiniteHelminthMaterials cheat (#985)
Closes #728

Reviewed-on: OpenWF/SpaceNinjaServer#985
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-22 11:09:17 -08:00
a3873a1710 fix(webui): show names for zaw parts (#988)
Reviewed-on: OpenWF/SpaceNinjaServer#988
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-02-22 02:22:41 -08:00
73f8f93b17 chore: update russian translation (#987)
Reviewed-on: OpenWF/SpaceNinjaServer#987
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-02-22 02:21:57 -08:00
df70050cfd chore: update translations 2025-02-22 07:10:28 +01:00
9f0be223e6 fix: returning givekeychainitem response 2025-02-21 15:46:09 +01:00
eb56442d63 fix: junction rewards (#982)
Reviewed-on: OpenWF/SpaceNinjaServer#982
Co-authored-by: Ordis <134585663+OrdisPrime@users.noreply.github.com>
Co-committed-by: Ordis <134585663+OrdisPrime@users.noreply.github.com>
2025-02-21 06:32:05 -08:00
a259afe912 feat(webui): give all quests (#981)
Reviewed-on: OpenWF/SpaceNinjaServer#981
Co-authored-by: Ordis <134585663+OrdisPrime@users.noreply.github.com>
Co-committed-by: Ordis <134585663+OrdisPrime@users.noreply.github.com>
2025-02-21 05:30:13 -08:00
4d7b3b543b fix: typings not matching reality 2025-02-21 08:29:42 +01:00
78548a2ebe chore: cleanup config (#979)
Reviewed-on: OpenWF/SpaceNinjaServer#979
Co-authored-by: Ordis <134585663+OrdisPrime@users.noreply.github.com>
Co-committed-by: Ordis <134585663+OrdisPrime@users.noreply.github.com>
2025-02-20 06:16:40 -08:00
815d18623e chore: generate inventory equipment types from equipmentKeys (#972)
Reviewed-on: OpenWF/SpaceNinjaServer#972
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-20 02:58:57 -08:00
ac6ac19199 chore: properly type equipment in IInventoryChanges (#973)
Reviewed-on: OpenWF/SpaceNinjaServer#973
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-20 02:58:44 -08:00
b4e780baa3 fix: save LoreFragmentScans (#974)
Reviewed-on: OpenWF/SpaceNinjaServer#974
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-20 02:57:23 -08:00
fb8d176fbe fix(webui): quest cheats (#965)
Completing Quests via the webui will now also award the quest's items and mails.

Also fixes doubly adding key chain items.
A few items will not be added, as it is currently impossible to determine the item category by path for these items.
This will be fixed soon.

Reviewed-on: OpenWF/SpaceNinjaServer#965
Co-authored-by: Ordis <134585663+OrdisPrime@users.noreply.github.com>
Co-committed-by: Ordis <134585663+OrdisPrime@users.noreply.github.com>
2025-02-19 14:09:47 -08:00
b551563681 fix: save settings when accepting trade policy. (#966)
![image.png](/attachments/b9954b5f-5ece-4803-b728-548ca2320fdf)

Co-authored-by: Kenya-DK <kenni.k@hotmail.com>
Reviewed-on: OpenWF/SpaceNinjaServer#966
Co-authored-by: CyberVenom <cybervenom@noreply.localhost>
Co-committed-by: CyberVenom <cybervenom@noreply.localhost>
2025-02-19 14:09:02 -08:00
ca4017ad1e chore: typings (#971)
Reviewed-on: OpenWF/SpaceNinjaServer#971
2025-02-19 14:07:28 -08:00
dee302c996 chore: handle motorcycle in addItems (#970)
Closes #968

Reviewed-on: OpenWF/SpaceNinjaServer#970
2025-02-19 13:53:21 -08:00
6acb0f5dca chore: enforce that account only owns one of 'singleton items' (#969)
Reviewed-on: OpenWF/SpaceNinjaServer#969
2025-02-19 13:42:36 -08:00
00a75a33fa fix: don't use path-based matching to add QuestKeys (#967)
Reviewed-on: OpenWF/SpaceNinjaServer#967
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-19 12:42:21 -08:00
1413a6bcc2 feat: move quest cheats to webui (#963)
Co-authored-by: Sainan <sainan@calamity.inc>
Reviewed-on: OpenWF/SpaceNinjaServer#963
Co-authored-by: Ordis <134585663+OrdisPrime@users.noreply.github.com>
Co-committed-by: Ordis <134585663+OrdisPrime@users.noreply.github.com>
2025-02-18 17:14:42 -08:00
87cc2594c8 fix: add missing quest keys at updateQuestKey (#958)
it's possible the quest key was not in already in the inventory but the quest was still available due to unlockAllQuests

Closes #957

Reviewed-on: OpenWF/SpaceNinjaServer#958
2025-02-18 13:48:21 -08:00
cd100c87b8 fix: respect purchaseQuantity when giving gear items from inbox message (#960)
Closes #942

Reviewed-on: OpenWF/SpaceNinjaServer#960
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-18 05:39:45 -08:00
c8542c9d75 chore: update PE+, add countedAtt to key chain triggered messages (#959)
Reviewed-on: OpenWF/SpaceNinjaServer#959
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-18 05:39:24 -08:00
a62e8eebc2 chore: log missionInventoryUpdate request body (#961)
there's still so much uncertainty about this, this is vital information to have logged by default, imo

Reviewed-on: OpenWF/SpaceNinjaServer#961
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-18 05:24:28 -08:00
0e7c124d26 fix: unable to add legendary core (#955)
Related to #952

Reviewed-on: OpenWF/SpaceNinjaServer#955
2025-02-12 18:15:22 -08:00
7ee8252d0e chore: prettier (#954)
some of the latest changes haven't been quite congruent with the way prettier would like things to be. this fixes those details.

Reviewed-on: OpenWF/SpaceNinjaServer#954
2025-02-12 18:15:07 -08:00
edddc80bd8 fix(webui): don't give legendary cores with "add missing mods" (#953)
Closes #952

Reviewed-on: OpenWF/SpaceNinjaServer#953
2025-02-12 18:14:59 -08:00
7e7e4e2eea feat: change dojo spawn room (#949)
Closes #524

Reviewed-on: OpenWF/SpaceNinjaServer#949
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-12 14:06:48 -08:00
eace26b4b3 chore: update PE+ (#951)
lavos prime and stuff

Reviewed-on: OpenWF/SpaceNinjaServer#951
2025-02-12 10:34:28 -08:00
947dcdcec5 chore: update PE+ (#950)
Closes #940

Reviewed-on: OpenWF/SpaceNinjaServer#950
2025-02-11 21:27:20 -08:00
2dade02f3e feat(stats): log unknown categories in updateStats (#947)
Reviewed-on: OpenWF/SpaceNinjaServer#947
Reviewed-by: Sainan <sainan@calamity.inc>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-02-11 21:27:05 -08:00
cf50738d34 feat: setDojoComponentMessage (#948)
Closes #946

Reviewed-on: OpenWF/SpaceNinjaServer#948
2025-02-11 20:11:31 -08:00
dc4d592b5a chore: fix order in api.ts 2025-02-12 00:22:05 +01:00
b3b2ce5524 fix(webui): remove 'step' from number inputs (#944)
browsers seem to validate that the value is a multiple of the step size, which was not the intention here

Reviewed-on: OpenWF/SpaceNinjaServer#944
2025-02-11 08:22:43 -08:00
61471d6785 chore: update nightwave to vol. 8 (#941)
Reviewed-on: OpenWF/SpaceNinjaServer#941
2025-02-11 08:22:37 -08:00
30061fb0e3 fix: don't abort quest update when quest completion rewards are missing. (#937)
Temporary fix until quest completion items are added. This is wip.
Your account has to own the quest keys for the quest system to work.

Reviewed-on: OpenWF/SpaceNinjaServer#937
Co-authored-by: Ordis <134585663+OrdisPrime@users.noreply.github.com>
Co-committed-by: Ordis <134585663+OrdisPrime@users.noreply.github.com>
2025-02-09 14:37:25 -08:00
a03c987f69 chore: handle client requesting non-Lotus assets (#934)
Reviewed-on: OpenWF/SpaceNinjaServer#934
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-09 09:39:52 -08:00
7863833850 fix: save nightwave challenges & sortie/archon hunt completion (#933)
Closes #932, Closes #468

Reviewed-on: OpenWF/SpaceNinjaServer#933
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-02-09 09:39:45 -08:00
4398d37566 chore: update localization files (#935)
Reviewed-on: OpenWF/SpaceNinjaServer#935
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-02-09 08:26:36 -08:00
7c59d4fe3f feat(webui): currencies (#931)
Closes #854

Reviewed-on: OpenWF/SpaceNinjaServer#931
2025-02-09 07:17:42 -08:00
4504b95977 feat(import): EvolutionProgress (#930)
Closes #929

Reviewed-on: OpenWF/SpaceNinjaServer#930
2025-02-08 22:22:30 -08:00
90b6d13923 feat(webui): change SupportedSyndicate (#923)
Closes #829

Reviewed-on: OpenWF/SpaceNinjaServer#923
Reviewed-by: Sainan <sainan@calamity.inc>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-02-08 22:22:22 -08:00
3d62fc4259 fix: save tailorshop customisations (#927)
Reviewed-on: OpenWF/SpaceNinjaServer#927
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-08 17:41:33 -08:00
d4c5e367b4 fix: nightmare missions rewards (#926)
Closes #416

Reviewed-on: OpenWF/SpaceNinjaServer#926
Reviewed-by: Sainan <sainan@calamity.inc>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-02-08 17:41:21 -08:00
eb3acad598 feat(vs-code): Debugging (#924)
Reviewed-on: OpenWF/SpaceNinjaServer#924
Co-authored-by: Ordis <134585663+OrdisPrime@users.noreply.github.com>
Co-committed-by: Ordis <134585663+OrdisPrime@users.noreply.github.com>
2025-02-07 09:49:13 -08:00
079f9ebbdf fix(webui): max rank up all suits (#917)
Fixes #914

Reviewed-on: OpenWF/SpaceNinjaServer#917
Reviewed-by: Sainan <sainan@calamity.inc>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-02-07 06:46:07 -08:00
0c1624cc03 chore: remove console logs (#922)
Reviewed-on: OpenWF/SpaceNinjaServer#922
Co-authored-by: Ordis <134585663+OrdisPrime@users.noreply.github.com>
Co-committed-by: Ordis <134585663+OrdisPrime@users.noreply.github.com>
2025-02-07 04:53:26 -08:00
9539bcf8ee fix: setting active quest (#921)
fixes #920

Reviewed-on: OpenWF/SpaceNinjaServer#921
Co-authored-by: Ordis <134585663+OrdisPrime@users.noreply.github.com>
Co-committed-by: Ordis <134585663+OrdisPrime@users.noreply.github.com>
2025-02-07 04:44:45 -08:00
9bff05a635 chore: update PE+ (#919)
Rhino Heirloom and stuff

Reviewed-on: OpenWF/SpaceNinjaServer#919
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-07 02:07:26 -08:00
e8559bc09c fix: don't add unknown skin items to RawUpgrades (#918)
Reviewed-on: OpenWF/SpaceNinjaServer#918
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-07 02:07:18 -08:00
0f4c14531b fix(webui): weird navbar margins at smaller widths 2025-02-06 21:55:42 +01:00
0fbf300d3e refactor: don't pass undefined to getRandomMissionRewards (#913)
Reviewed-on: OpenWF/SpaceNinjaServer#913
Co-authored-by: Ordis <134585663+OrdisPrime@users.noreply.github.com>
Co-committed-by: Ordis <134585663+OrdisPrime@users.noreply.github.com>
2025-02-06 07:11:31 -08:00
1fd801403f fix(webui): lowercase email address to match client (#912)
Reviewed-on: OpenWF/SpaceNinjaServer#912
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-06 07:10:25 -08:00
78032f191c feat(webui): translations (#909)
Closes #900
Supersedes #903

Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>

Reviewed-on: OpenWF/SpaceNinjaServer#909
2025-02-06 07:00:21 -08:00
13c68a75c1 feat: initial stats save (#884)
Closes #203

Reviewed-on: http://209.141.38.3/OpenWF/SpaceNinjaServer/pulls/884
Reviewed-by: Sainan <sainan@calamity.inc>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-02-06 04:42:59 -08:00
8175deb023 chore: get rid of instances of markModified (#908)
Closes #904

Reviewed-on: http://209.141.38.3/OpenWF/SpaceNinjaServer/pulls/908
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-06 04:39:01 -08:00
1c82b90033 feat: obtaining crewship related items on mission update (#897)
Reviewed-on: http://209.141.38.3/OpenWF/SpaceNinjaServer/pulls/897
Reviewed-by: Sainan <sainan@calamity.inc>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-02-05 12:23:35 -08:00
d396fe8b5c fix: handle acquisition of modular weapon parts (#906)
Fixes #905

Reviewed-on: http://209.141.38.3/OpenWF/SpaceNinjaServer/pulls/906
2025-02-05 09:00:20 -08:00
1351e73961 chore(webui): clarify what credentials are required (#902)
Reviewed-on: http://209.141.38.3/OpenWF/SpaceNinjaServer/pulls/902
2025-02-05 06:37:31 -08:00
4353c67867 fix: delete inbox messages when deleting account (#899)
Reviewed-on: http://209.141.38.3/OpenWF/SpaceNinjaServer/pulls/899
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-05 05:54:24 -08:00
8633696dc8 chore: update tunablesController (#901)
Reviewed-on: http://209.141.38.3/OpenWF/SpaceNinjaServer/pulls/901
2025-02-04 19:13:48 -08:00
a5d74b92c8 feat(import): Consumables (#895)
Closes #894

Reviewed-on: http://209.141.38.3/OpenWF/SpaceNinjaServer/pulls/895
2025-02-04 09:19:14 -08:00
f15f2bfdbd chore: update favicon (#896)
This change is paired with a change in the bootstrapper to make the icons all unique and somewhat resembling their part in the whole.

![image.webp](/attachments/b30a31d9-15bd-4933-93cb-a409a9c91159)

Reviewed-on: http://209.141.38.3/OpenWF/SpaceNinjaServer/pulls/896
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-04 06:39:28 -08:00
c1fcd3042e feat(webui): ensure forma count of at least 5 when max ranking item (#893)
Closes #889

Reviewed-on: http://209.141.38.3/OpenWF/SpaceNinjaServer/pulls/893
2025-02-04 03:22:37 -08:00
fb232f74bd feat: acquiring CrewShipHarness with CrewShip (#888)
Closes #886

Reviewed-on: http://209.141.38.3/OpenWF/SpaceNinjaServer/pulls/888
Reviewed-by: Sainan <sainan@calamity.inc>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-02-04 02:29:23 -08:00
c267ce47c3 update docker-compose.yml 2025-02-03 22:55:26 +01:00
3537c7e436 add docker workflow using docker hub as remote 2025-02-03 22:50:35 +01:00
3b3edaced4 fix: universalPolarityEverywhere not affecting all necramech slots (#891)
Fixes #890

Reviewed-on: http://209.141.38.3/OpenWF/SpaceNinjaServer/pulls/891
2025-02-03 13:21:12 -08:00
e46b3c7d29 chore: use mongoose's 'id' function in addGearExpByCategory (#892)
Reviewed-on: http://209.141.38.3/OpenWF/SpaceNinjaServer/pulls/892
2025-02-03 13:20:56 -08:00
241f0c894a chore(webui): remove client cheats (#883)
This has long been only a very small subset of what the bootstrapper offers. I think it's better that the bootstrapper itself provides the interface for it and we don't duplicate the logic so shallowly.

Reviewed-on: http://209.141.38.3/OpenWF/SpaceNinjaServer/pulls/883
2025-02-03 12:10:36 -08:00
9823729aa8 chore: update batch script 2025-02-02 14:30:51 +01:00
07451dcef0 fix: inventory not being requested when visiting navigation (#882)
Reviewed-on: http://209.141.38.3/OpenWF/SpaceNinjaServer/pulls/882
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-02 05:16:43 -08:00
d62ef9bbf3 fix: don't give level mission credits on free roam missions (#881)
Reviewed-on: http://209.141.38.3/OpenWF/SpaceNinjaServer/pulls/881
Co-authored-by: Ordis <134585663+OrdisPrime@users.noreply.github.com>
Co-committed-by: Ordis <134585663+OrdisPrime@users.noreply.github.com>
2025-02-01 08:20:11 -08:00
5460ccf93d feat: loc-pin saving (#879)
Closes #404

Co-authored-by: Sainan <sainan@calamity.inc>
Reviewed-on: http://209.141.38.3/OpenWF/SpaceNinjaServer/pulls/879
Reviewed-by: Sainan <sainan@noreply.localhost>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-02-01 07:41:34 -08:00
53ce6ccce2 fix: subtract standing gained in missions from daily bin (#880)
Fixes #794

Reviewed-on: http://209.141.38.3/OpenWF/SpaceNinjaServer/pulls/880
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-01 07:32:56 -08:00
edc3171eee feat: Quests 2 (#878) 2025-02-01 16:31:04 +01:00
0f250c6103 remove .coderabbit.yaml 2025-02-01 14:11:47 +01:00
61e4ab1934 remove docker workflow 2025-02-01 12:55:14 +01:00
01d369bf38 fix: can't log in to existing account 2025-01-31 17:15:58 +01:00
aca0b0fe4c
feat: earning intrinsics (#872) 2025-01-31 17:03:14 +01:00
9ab0d8d15e
feat: startLibraryPersonalTarget (#873) 2025-01-31 17:03:00 +01:00
3a7cb5d9b1
fix: correctly add kubrow eggs to inventory (#875) 2025-01-31 17:02:46 +01:00
9de87f0959
fix: 'account now owns a negative amount' not showing when it had 0 (#877) 2025-01-31 17:02:27 +01:00
50c280cf01
feat: Inbox (#876) 2025-01-31 14:15:36 +01:00
cf196430b7 fix: sort api imports alphabetically 2025-01-31 09:37:51 +01:00
de7758684b
feat: earn focus xp with a lens (#871) 2025-01-27 18:11:05 +01:00
97bec71b05
feat: more supported equipment types (#867) 2025-01-27 13:18:16 +01:00
cb7c15a382
fix: provide LoadOutPresets & Ships in missionInventoryUpdate response (#869) 2025-01-25 13:12:49 +01:00
6a427018e3
fix: can't acquire Sun & Moon (#865) 2025-01-25 06:25:13 +01:00
b72a0d12ef
fix: apply spoofing stuff to missionInventoryUpdate's InventoryJson (#866) 2025-01-24 21:09:34 +01:00
57061073be
fix: adjust mission update controller to add xp when aborting mission(#864) 2025-01-24 16:17:59 +01:00
080b466bfc
fix(webui): add items (#863) 2025-01-24 16:12:39 +01:00
3cd66391b6
fix(webui): max rank (#859) 2025-01-24 15:44:34 +01:00
5649c5bf86
chore: switch purchaseService to take inventory document (#848) 2025-01-24 15:24:29 +01:00
249d2056ed fix: use logger instead of console 2025-01-24 15:20:51 +01:00
ebd51cc380
fix: aborting Mission and completeAllQuests config (#858) 2025-01-24 15:13:55 +01:00
8b836020bf
chore: turn getJSONfromString into a template/generic function (#836) 2025-01-24 14:27:10 +01:00
61f63dd40f
fix: typescript version unsupported by eslint (#853) 2025-01-24 14:23:40 +01:00
4e8c079171
fix: exclude riven buffs from being a curse (#849) 2025-01-24 14:18:16 +01:00
efcaaa56c4
fix: tell client when it has used a free favor (#850) 2025-01-24 14:18:05 +01:00
7716c945d0
fix: address some client warnings about malformed inventory.php response (#840) 2025-01-24 14:17:52 +01:00
ef2708b510
feat: Quests1 (#852) 2025-01-24 14:13:21 +01:00
8858b15693
fix: rectify CrewMembers import & typings (#845) 2025-01-21 20:07:15 +01:00
90f05c477b
fix: slight logic error in importService (#846) 2025-01-21 20:05:28 +01:00
25a099970e fix(webui): delete prompt showing name twice 2025-01-21 18:36:45 +01:00
1ba3378574
fix(webui): properly handle unique level caps (#837) 2025-01-20 18:29:25 +01:00
7b78f5a997
fix(starter-bat): automatically prune references that no longer exist remotely (#838) 2025-01-20 18:28:56 +01:00
10100ae2ca
fix: more accurate inventory after skipTutorial (#755) 2025-01-20 18:25:50 +01:00
dependabot[bot]
701cd19a17
build(deps-dev): bump eslint-plugin-prettier from 5.2.1 to 5.2.3 (#842)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-20 17:29:08 +01:00
7bb857a17a fix(webui): disable text wrapping for action column 2025-01-20 12:35:26 +01:00
3eab0c187c
feat: add update and start script for dummies (#828) 2025-01-20 12:22:34 +01:00
62a4ac0652
feat(webui): add necramechs (#834) 2025-01-20 12:21:50 +01:00
a4c44e8bb0
chore: get rid of some unecessary conditionals (#835) 2025-01-20 12:21:39 +01:00
c9b48ace36
feat: import (#831) 2025-01-20 12:19:32 +01:00
ee0bee5d7b fix(webui): shrink count inputs 2025-01-20 05:23:32 +01:00
ceba8252b0
feat(webui): mod count (#830) 2025-01-20 04:00:58 +01:00
b19fda66a2 fix(webui): ignore empty ModularParts array 2025-01-19 15:06:32 +01:00
86a2b57e22
feat(webui): more equipment (#826) 2025-01-19 15:03:34 +01:00
3c99b748dc chore: raise size limit for text/plain requests 2025-01-19 12:33:29 +01:00
1c186450e1 chore: remove unused IInventoryResponseDocument 2025-01-19 12:33:26 +01:00
f310028e42
fix: classical syndicate medallions don't use daily limit (#818) 2025-01-19 12:30:27 +01:00
16922279f9
fix: remove legendary core from inventory when it was used (#819) 2025-01-19 12:29:53 +01:00
4ce03ad523
chore(webui): split weapons by category (#820) 2025-01-19 12:29:32 +01:00
45c32b087d
chore: handle addMiscItems & addMods resulting in account having 0 or less of a type (#821) 2025-01-19 12:29:19 +01:00
15f36263cd
chore: make buildConfig.json optional (#822) 2025-01-19 12:28:45 +01:00
ae832d0125 fix(webui): erroring on empty archon shard slot 2025-01-19 03:58:25 +01:00
d25a969269
chore: optimise stats/view.php (#816) 2025-01-19 01:58:59 +01:00
5d4c454b0b
chore: optimise creditsController (#815) 2025-01-19 01:58:47 +01:00
73df848f11
chore: optimise getAccountIdForRequest (#814) 2025-01-19 01:58:35 +01:00
16c2b8f83c
chore: use MongooseDocumentArray.id instead of .find where possible (#813) 2025-01-19 01:58:24 +01:00
7af5cd9811
fix: add slots when adding items via WebUI (#812) 2025-01-19 01:58:09 +01:00
a10bdeb497
feat: rerolling rivens (#806) 2025-01-19 01:57:52 +01:00
fc8537ba4d
feat(webui): add "add missing mods" (#804)
Co-authored-by: Chinosu <46995931+Chinosu@users.noreply.github.com>
2025-01-19 01:57:39 +01:00
a8fb9095c5
feat: Kinematic Instant Messaging (#801) 2025-01-19 01:57:24 +01:00
f1c3dcbefc
chore: move mod upgrading logic into artifactsController (#800) 2025-01-18 11:12:06 +01:00
734ca84557
fix: purchasing flawed mods from iron wake (#802) 2025-01-18 11:11:52 +01:00
79299db475
fix: not consuming ItemPrices from server-side vendor (#798) 2025-01-18 07:06:07 +01:00
15193603e3 chore: npm audit fix 2025-01-18 07:05:03 +01:00
1e4092e7f8 feat(webui): ability to add Legendary Core 2025-01-17 16:26:48 +01:00
9fd2fb6ba2
fix: track FreeFavorsEarned & FreeFavorsUsed (#792) 2025-01-17 14:43:51 +01:00
79f1937483
fix: handle standing limits in fishmongerController (#795) 2025-01-17 14:43:33 +01:00
0ace5eb446
fix: identify correct offer for when teshin has 2 kuva offers up (#797) 2025-01-17 14:43:09 +01:00
b3d2345894 fix: steel path honors having expired stuff 2025-01-17 13:22:29 +01:00
d5d60bcbff chore: improve IVendorManifest 2025-01-17 13:22:27 +01:00
29206f142d chore: simplify computation of allDailyAffiliationKeys 2025-01-17 07:25:15 +01:00
1a8e0f33b9
feat: noDailyStandingLimits cheat (#791) 2025-01-17 07:02:19 +01:00
d8845bc478
feat: apply & track daily standing limit when trading in medallions (#788) 2025-01-17 05:27:12 +01:00
534f7d8cce
feat: archon shard fusion (#785) 2025-01-17 05:09:25 +01:00
6ee28e5864
fix: syndicate sacrifice doesn't persist new title (#787) 2025-01-17 05:09:11 +01:00
9633d307a2 fix: incomplete circuit weapon names 2025-01-16 09:21:04 +01:00
a545d4f047
feat(webui): add "max rank all warframes" & "max rank all weapons" (#783)
Co-authored-by: Sainan <sainan@calamity.inc>
2025-01-15 16:29:02 +01:00
7d7466cbc1
chore: update nightwave to nora's mix vol. 7 (#784)
Co-authored-by: Sainan <sainan@calamity.inc>
2025-01-15 16:23:58 +01:00
fc7eaa0283
fix: incarnon options cycle (#782) 2025-01-15 16:18:42 +01:00
d73d14bc48
feat: add potatoes, exilus, & arcanes everywhere cheats (#774) 2025-01-15 05:20:30 +01:00
215b83974c
feat(webui): add "add missing warframes" & "add missing weapons" (#775) 2025-01-15 05:20:17 +01:00
eab4eb2e5b fix(webui): spacing 2025-01-15 05:19:27 +01:00
26f20bfbb5
fix: limit standing gain from medallions for title's max (#772) 2025-01-13 17:57:59 +01:00
dependabot[bot]
4698578599
build(deps): bump warframe-public-export-plus from 0.5.21 to 0.5.22 (#780)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-13 17:54:20 +01:00
dependabot[bot]
a8d5bafc29
build(deps): bump mongoose from 8.9.3 to 8.9.4 (#779)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-13 17:51:35 +01:00
dependabot[bot]
a988f3e899
build(deps-dev): bump typescript from 5.5.3 to 5.5.4 (#778)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-13 17:51:18 +01:00
5d43627805
fix: 1999 calendar not working properly (#777) 2025-01-13 04:17:06 +01:00
e201279eee
fix: remove ship decos from inventory when placed and vice-versa (#770) 2025-01-12 13:38:05 +01:00
5cbececb04
fix(webui): error on unrevealed riven mod (#773) 2025-01-12 08:30:56 +01:00
8ebd7068e2
fix: premature week rollover (#771) 2025-01-12 05:54:52 +01:00
2cd47c8ae2
fix: reproducible oids for unlockAllSkins (#769) 2025-01-12 02:42:27 +01:00
53d5e7c3f0
fix: make crew member slots optional (#766) 2025-01-11 23:08:17 +01:00
f6265d57ec
feat: Sentient Anomaly rotation (#759)
Co-authored-by: Sainan <sainan@calamity.inc>
2025-01-11 23:01:33 +01:00
25459503d1
feat: changing equipped shawzin/instrument (#762) 2025-01-11 12:54:32 +01:00
e8e918ff0c
fix: purchasing of ship decorations (#761) 2025-01-11 12:54:11 +01:00
fb8e19403e
feat: cycle 1999 calendar season every week (#756) 2025-01-11 07:18:42 +01:00
eafdd9f755
fix: worldState growing with every request (#760) 2025-01-10 06:23:25 +01:00
1c654650d4
fix: cap helminth resources at 100% (#757) 2025-01-09 14:02:12 +01:00
c07f7502a4 fix(coderabbit): disable commit_status as it now indicates failure on rate limit 2025-01-09 07:30:33 +01:00
56906a0f69 chore: npm run prettier 2025-01-09 07:17:29 +01:00
cd5aaaa6cf
fix: change cavia bounties every 2.5 hours (#748) 2025-01-09 05:54:29 +01:00
eeaa339090
fix: can't open nightwave offerings (#754) 2025-01-08 19:34:38 +01:00
e4476d7136
fix: add coalescent shards segment to allShipFeatures (#743) 2025-01-07 04:56:39 +01:00
dependabot[bot]
abf312a41b
build(deps): bump mongoose from 8.9.2 to 8.9.3 (#746)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-06 23:36:19 +01:00
172db2337f
feat: infiniteEndo & infiniteRegalAya (#741) 2025-01-06 05:36:39 +01:00
f56fc232f2
feat: leveling up intrinsics (#725) 2025-01-06 05:36:18 +01:00
83617ae287
feat: activate riven mod (get a random challenge) (#721) 2025-01-06 05:35:57 +01:00
3903b973e2
feat: opening relics in endless missions (#713) 2025-01-06 05:35:36 +01:00
8d7f69ce80
fix: motorcycle in backroom is broken (#736) 2025-01-06 05:35:11 +01:00
2c875caddc
fix: remove syndicate sacrifices from inventory (#735) 2025-01-06 05:34:59 +01:00
3a4eea379d
chore: move sell types to sellController (#731) 2025-01-06 05:34:37 +01:00
05fd3c4cec chore: some notes in inventoryTypes 2025-01-06 05:27:12 +01:00
69c65f3ce2
fix: missing teshin hard mode vendor manifest (#737) 2025-01-06 04:36:40 +01:00
709c2a401b
feat: spectre loadouts (#719) 2025-01-06 01:21:37 +01:00
eb6baa5e15
fix: removing an archon shard doesn't refund it (#729) 2025-01-06 01:21:02 +01:00
82621ebe0f
feat: sentient apetite (#726) 2025-01-05 23:20:36 +01:00
9d115a4d02
feat: archon shard removal (#724) 2025-01-05 13:40:19 +01:00
d69ebf89ec
feat: helminth losing apetite (#718) 2025-01-05 13:34:41 +01:00
1bab76f58b
fix: unlockAllScans not fully working with blacklisted enemies (#723) 2025-01-05 12:37:08 +01:00
8154f9bc36
feat(webui): add "Fully Level Up Helminth" (#717) 2025-01-05 12:26:26 +01:00
06bc0123ba
feat: all server-side metamorphosis levels (#716) 2025-01-05 07:16:48 +01:00
506e77db6c
feat: invigorations (#715) 2025-01-05 06:17:42 +01:00
6baad5d008
feat: correctly scale standing and focus limits by mastery rank (#711) 2025-01-05 05:17:56 +01:00
05d16f09b6
feat: handle helminth offerings update request (#714) 2025-01-05 05:17:40 +01:00
e42e2eb258 chore: npm run prettier 2025-01-05 05:16:45 +01:00
595305081a fix: wrong format for "log-in expired" response 2025-01-05 05:04:50 +01:00
ea59665a0c
chore: implement /pay/getSkuCatalog.php (#706) 2025-01-05 02:44:01 +01:00
bd7baef002
fix(webui): diambiguate fish names (#705) 2025-01-05 02:43:22 +01:00
27ddada3f3
fix: quantity ignored when purchasing slots (#704) 2025-01-05 02:43:06 +01:00
571d244985 chore: rename getCreditsController to creditsController 2025-01-04 01:04:58 +01:00
76d40964db chore: remove log-in expired handler in getCreditsController 2025-01-04 01:04:00 +01:00
e77f8b0e51
feat: replicate dojo research (#701) 2025-01-04 00:25:23 +01:00
74ed098692
chore: do addItem on inventory document, not accountId (#699) 2025-01-04 00:25:09 +01:00
7a6ffd94dc fix: error handler registered in the wrong place 2025-01-03 22:46:14 +01:00
4756f54f40
chore: add middleware for error handling (#695) 2025-01-03 22:25:03 +01:00
e6432b5052
chore: cleanup inventory types (#691) 2025-01-03 22:17:34 +01:00
69734ea101
fix: don't say "error" just because a loadout category is unimplemented (#692) 2025-01-03 09:19:06 +01:00
0523fbdaae
fix: add missing kitgun types for primaries (#694) 2025-01-03 09:09:53 +01:00
e0ff240d60
feat: dojo research (#689) 2025-01-03 09:06:50 +01:00
c80dd1bbd0
feat: remove incarnon (#688) 2025-01-03 09:06:34 +01:00
f1c0c5a429
feat: subsuming warframes (#686) 2025-01-03 05:22:56 +01:00
ff4b1e5c29
fix: enable completeAllQuests in default/example config (#684) 2025-01-03 00:49:18 +01:00
80e00b8825
fix: consume resources when installing incarnon genesis (#687) 2025-01-03 00:48:54 +01:00
b8ceb78c98
feat: trade fish for standing (#681) 2025-01-03 00:10:18 +01:00
e7a9f2e2b8
chore: move syndicate sacrifice stuff into syndicateSacrificeController (#682) 2025-01-03 00:05:34 +01:00
52d1b72701
fix: selling MiscItems doesn't remove them from inventory (#680) 2025-01-02 08:55:04 +01:00
0c6f6e556f
feat: infusing abilities (#676) 2025-01-02 08:54:27 +01:00
48aa145a20
fix: error when attempting to sell items for ducats (#678) 2024-12-31 21:17:46 +01:00
3e54977d4b
feat: helminth gaining subsume slots (#677) 2024-12-31 04:46:12 +01:00
a16158aedd
chore: simplify upgradesController (#675) 2024-12-31 02:50:11 +01:00
e4613069b3
fix: abort startup if not connected to MongoDB server (#665) 2024-12-31 01:41:47 +01:00
16d98636e9
chore: updateCurrency with existing inventory instance (#674) 2024-12-31 01:41:29 +01:00
ddb1a8d665
fix(webui): showing hidden recipes for "add items" (#672) 2024-12-31 01:40:32 +01:00
0e1ee0c669
fix: purchase of multiple booster packs (#671) 2024-12-31 01:39:45 +01:00
230c0303b1
chore: remove recipeService (#659) 2024-12-31 01:36:28 +01:00
b8ef39bada
feat: implement setShipFavouriteLoadout.php (#662) 2024-12-30 19:48:43 +01:00
d930c3d957
feat: fish dissection (#663) 2024-12-30 19:48:20 +01:00
dependabot[bot]
7ea02d142f
build(deps-dev): bump @typescript-eslint/eslint-plugin from 7.15.0 to 7.18.0 (#667)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-30 19:44:05 +01:00
dependabot[bot]
5b1e162c1c
build(deps): bump warframe-public-export-plus from 0.5.16 to 0.5.17 (#666)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-30 19:43:27 +01:00
1024d0350f
fix: consistenly use static/data for 'npm run build' (#661) 2024-12-30 01:51:38 +01:00
e41022f176 chore: improve typings in trainingResultsController 2024-12-30 00:09:03 +01:00
9455703bdc chore: fix non-string concat warnings 2024-12-30 00:07:53 +01:00
1c436d9466 chore: fix unsafe assignment of any value warning 2024-12-30 00:06:31 +01:00
212a5e7035 fix(webui): 404 for favicon.ico with 'npm run build' 2024-12-29 23:44:38 +01:00
02f4d0e821
improve: handle config.administratorNames being a string (#658) 2024-12-29 23:34:26 +01:00
f0eea818f9 chore: fix unsafe assignment of any value warning 2024-12-29 23:33:02 +01:00
412968026e chore: add ability range & efficiency for archon crystal upgrades 2024-12-29 23:08:21 +01:00
d31f9f8d24
chore: fix most explicit-function-return-type warnings (#656) 2024-12-29 21:47:18 +01:00
05be199927
chore: fix "member access .toString on any value" warnings (#655) 2024-12-29 21:41:56 +01:00
25c8179a88
chore: remove toLoginRequest (#651) 2024-12-29 21:41:39 +01:00
9e21105474
chore: improve IInventoryChanges (#654) 2024-12-29 21:40:54 +01:00
00bcf5c3c5
chore: fix unsafe member access warnings for upgrade fingerprints (#653) 2024-12-29 21:40:38 +01:00
8a4f2f4d0e
chore: improve IFindSessionRequest (#652) 2024-12-29 21:40:25 +01:00
44b78ecfe8
feat: unlock all captura scenes (#650) 2024-12-29 21:11:36 +01:00
607ec836e9
fix: tutorial being skipped with skipTutorial disabled (#613) 2024-12-29 21:11:10 +01:00
9dbb0fe4bf
improve: for "make rank 30", also make respective exalted items rank 30 (#648) 2024-12-29 06:37:40 +01:00
27af54d039 chore: fix concat of ObjectId to make eslint happy 2024-12-29 06:14:54 +01:00
b0c3e725f8 chore: fix no-case-declarations warnings 2024-12-29 06:14:29 +01:00
dc85be8f37
fix: purchase response doesn't include exalted weapons when applicable (#647) 2024-12-29 03:42:22 +01:00
b5e0712675
fix: exalted weapons should not be duplicated as they are shared (#645) 2024-12-29 02:46:57 +01:00
3ae2338c13
fix: unable to spawn all enemies in simulacrum despite unlockAllScans (#642) 2024-12-28 18:31:10 +01:00
494f219db3
feat: dynamically cycle ESO, holdfast bounties, hex bounties, & circuit choices (#643) 2024-12-28 18:30:43 +01:00
4d1bbff99e
fix: booster packs not showing what items were gained after purchase (#635) 2024-12-25 23:34:14 +01:00
735f0b885d
feat: syndicate initiation (#638) 2024-12-25 23:33:29 +01:00
8fe9b89143
fix: no hex bounties available (#641) 2024-12-25 23:32:12 +01:00
3a1b407a81
fix: selling consumable/gear items (#639) 2024-12-25 01:08:18 +01:00
8ad979ab11
fix: can't dissolve arcanes (#634) 2024-12-23 23:32:33 +01:00
7fdb59f6c9
feat: dojo room energy & capacity costs & gains (#633) 2024-12-23 23:12:21 +01:00
063adb3519
improve: tell user that the WebUI is available (#631) 2024-12-23 22:48:16 +01:00
45cb9c6da0
fix: acquisition of railjack (#629) 2024-12-23 22:47:58 +01:00
7dcb1f4fa4
chore: initial documentation of config.json (#627) 2024-12-23 22:44:43 +01:00
42f11b2d30
fix: give respective weapons & mods when acquiring sentinel (#623) 2024-12-23 22:44:24 +01:00
103e9bc431
feat: add administrators, require administrator perms to change server config in webui (#628) 2024-12-23 22:44:01 +01:00
eeaac6f07e Revert "build(deps-dev): bump typescript from 5.5.3 to 5.7.2 (#626)"
This reverts commit d50c6b8c76c2d8eb8ea8c68cc0124e39b3ac8e3e.
2024-12-23 22:19:31 +01:00
dependabot[bot]
d50c6b8c76
build(deps-dev): bump typescript from 5.5.3 to 5.7.2 (#626)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-23 17:53:50 +01:00
dependabot[bot]
11d1daf206
build(deps-dev): bump eslint-plugin-prettier from 5.1.3 to 5.2.1 (#624)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-23 17:53:27 +01:00
77c7522023
feat(webui): ability to add recipes via "add items" (#617) 2024-12-23 14:37:21 +01:00
9be89fa9b7
feat(webui): rename account (#616) 2024-12-23 14:37:07 +01:00
0a4d620652
chore: update trainingResultController (#611) 2024-12-23 14:36:52 +01:00
dda41875ae
fix: acquiring ships (#619) 2024-12-23 09:15:41 +01:00
918e33f126
fix: incorrect types for PersonalRooms & TailorShop (#618) 2024-12-23 06:21:48 +01:00
68335aa91b
fix: handle purchaseQuantity for resources (#609) 2024-12-23 04:05:06 +01:00
ba7da656a8
feat(webui): delete account (#615) 2024-12-23 03:34:14 +01:00
066d07f8ba
fix: incomplete regex for stripped assets (#614) 2024-12-23 02:29:16 +01:00
d5c829e4fe
fix: avoid spilling new database account fields into login response (#610) 2024-12-23 00:40:35 +01:00
412de02680
feat: subtract standing for syndicate purchases (#608) 2024-12-22 23:31:30 +01:00
c421c7021c
feat: implement aya costs for varzia offers (#606) 2024-12-22 23:28:59 +01:00
d1d221bb58
feat: apply QuantityMultiplier for server-side vendor offers (#605) 2024-12-22 23:28:44 +01:00
2175e003cc style(coderabbit): disable related PRs 2024-12-22 22:14:50 +01:00
ce94c78cc1 fix: scale MiscItem prices by quantity 2024-12-22 22:14:08 +01:00
0a31ff7b5c
feat(webui): language selector (#593) 2024-12-22 20:38:50 +01:00
52c0a3123e
feat: implement syndicateStandingBonus endpoint (#583) 2024-12-22 20:37:02 +01:00
b84258a893
feat: basic implementation of endlessXp.php we can play The Circuit (#596) 2024-12-22 20:36:01 +01:00
9fd6ed3b21
fix: purchasing an arcane pack does not consume vosfor (#601) 2024-12-22 20:35:08 +01:00
ac09fcec5c
fix: don't default scale ship decorations to 1 (#603) 2024-12-22 20:34:04 +01:00
95bd07b50f
feat: decorating the backroom (#604) 2024-12-22 20:32:19 +01:00
cbdd1cd0a7 fix: unable to purchase arcanes 2024-12-22 16:15:05 +01:00
987b05a334
chore: update express to v5 (#599) 2024-12-22 15:42:24 +01:00
febe7ec5e0
feat: implement feeding of helminth (#597) 2024-12-22 07:26:14 +01:00
f2ae465dd9
fix: inconsistent handling of purchase request (#594) 2024-12-22 05:40:37 +01:00
c2a9fc6609
fix: unable to buy fish bait (#598) 2024-12-22 05:38:46 +01:00
c6ed013e23
fix: purchasing of augment mods (#595) 2024-12-22 05:32:30 +01:00
7b2c32b723 style(coderabbit): disable sequence diagrams 2024-12-22 05:13:43 +01:00
37f6fe9323 fix: isDate 2024-12-22 01:02:27 +01:00
0398691e01 chore: simplify updateGeneric 2024-12-22 01:02:25 +01:00
c6c3e1c005 chore: simplify getExalted 2024-12-22 01:02:21 +01:00
7b894823cc fix: duplicate warnings
explicit-module-boundary-types is a subset of explicit-function-return-type
2024-12-22 01:02:19 +01:00
dependabot[bot]
d1cf2953df
build(deps-dev): bump prettier from 3.3.2 to 3.4.2 (#592)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-22 00:59:20 +01:00
dependabot[bot]
b544d6159b
build(deps-dev): bump @typescript-eslint/parser from 7.15.0 to 7.18.0 (#494)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-22 00:55:25 +01:00
dependabot[bot]
46332bf3e0
build(deps): bump winston from 3.13.0 to 3.17.0 (#591)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-22 00:51:51 +01:00
dependabot[bot]
5ddc1aea85
build(deps): bump mongoose from 8.9.0 to 8.9.2 (#590)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-22 00:48:45 +01:00
神楽坂·喵
fe7051f855
Fix environment to config.json parser (#589) 2024-12-22 00:47:46 +01:00
d9c94664c3
feat: daily reset for syndicate standing (#582) 2024-12-22 00:44:49 +01:00
8a43eae230
feat: replace infiniteResources with infiniteCredits & infinitePlatinum (#588) 2024-12-22 00:34:19 +01:00
144ac5850c
feat(inventory): add accolade fields to IInventoryResponse (#586) 2024-12-20 03:12:36 +01:00
b0b2d9f6fa
fix: automatically populate regions for unlockAllMissions (#587) 2024-12-20 03:11:38 +01:00
d824b83cf9
feat: respect client-supplied version information (#585) 2024-12-20 03:11:09 +01:00
259bfa1362
chore: npm audit fix (#579) 2024-12-19 01:42:10 +01:00
0dd98393a5
feat(inventory&loadouts): motorcycles (#580) 2024-12-16 05:25:28 +01:00
cd514d47af
chore: update for 1999 (#576) 2024-12-16 04:50:51 +01:00
38e7d3d078
chore: update PE+ to 0.5.5 (#573) 2024-11-17 04:18:13 +01:00
Vampire Kitten
e98514a7be
improve: Add Ergo Blast's Tenet Weapon shop (#568) 2024-10-19 13:47:28 +02:00
1b95186ab8
chore: remove leftover console.log in inventoryController (#569) 2024-10-19 13:45:06 +02:00
Vampire Kitten
1a029ebb4b
fix: missing vendor infos (#565) 2024-10-18 18:13:53 +02:00
c20e3ea01d
chore: update warframe-riven-info (#553) 2024-10-18 17:03:24 +02:00
ba349535fb
feat: implement setPlacedDecoInfo (#558) 2024-10-18 16:54:49 +02:00
abc3bd8624
fix: being unable to visit Palladino in Iron Wake despite completeAllQuests (#564) 2024-10-18 16:49:33 +02:00
Vampire Kitten
76964585eb
fix: Apply Look not working with Unlock All Skins turned on (#549) 2024-10-15 16:27:58 +02:00
Vampire Kitten
59679c3d56
feat: Installation of Focus Lenses (#550) 2024-10-15 16:27:11 +02:00
07c2fbcadf
feat: implement socketing of ayatan sculptures (#542) 2024-10-12 23:51:45 +02:00
6c4c685690
chore: fix inconsistent formatting (npm run prettier) (#543) 2024-10-12 23:49:33 +02:00
cc5713e375
chore: handle resource being rolled as mission reward (#545) 2024-10-12 23:49:06 +02:00
de6c1da55d
fix: docker workflow failing in forks (#531) 2024-10-12 00:26:47 +02:00
26a5f31ee9
feat: add tunables endpoint (#530) 2024-10-12 00:26:19 +02:00
5fb4b94bb4
chore: update PE+ to 0.5.1 (#537) 2024-10-12 00:25:43 +02:00
sw5ciprl
533c249e68
feat: create Docker image, set up Docker CI (#528) 2024-10-10 22:07:37 +02:00
d9c95e676d
chore: update coderabbit config 2024-10-10 14:30:38 +02:00
0c31eb4b25
chore: config file for coderabbit 2024-10-07 17:45:07 +02:00
f9ed123cb4
fix: not showing "void fissures" tab in navigation (#521) 2024-10-07 17:44:02 +02:00
b7f381ba1d
feat: implement upgrading & downgrading arcanes (#520) 2024-10-06 17:43:43 +02:00
59389a991b
npm audit fix (#518) 2024-10-06 14:47:09 +02:00
59cec40443
chore: update warframe-public-export-plus for 37.0.0 (#517) 2024-10-02 23:18:39 +02:00
293 changed files with 32816 additions and 22766 deletions

19
.coderabbit.yaml Normal file
View File

@ -0,0 +1,19 @@
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
language: "en-US"
early_access: false
reviews:
profile: "chill"
request_changes_workflow: false
changed_files_summary: false
high_level_summary: false
poem: false
review_status: true
commit_status: false
collapse_walkthrough: false
sequence_diagrams: false
related_prs: false
auto_review:
enabled: true
drafts: false
chat:
auto_reply: true

5
.dockerignore Normal file
View File

@ -0,0 +1,5 @@
**/.dockerignore
**/.git
Dockerfile*
.*
docker-data/

View File

@ -1,4 +0,0 @@
# Docker may need a .env file for the following settings:
DATABASE_PORT=27017
DATABASE_USERNAME=root
DATABASE_PASSWORD=database

View File

@ -12,7 +12,6 @@
},
"rules": {
"@typescript-eslint/explicit-function-return-type": "warn",
"@typescript-eslint/explicit-module-boundary-types": "warn",
"@typescript-eslint/restrict-template-expressions": "warn",
"@typescript-eslint/restrict-plus-operands": "warn",
"@typescript-eslint/no-unsafe-member-access": "warn",
@ -23,10 +22,13 @@
"@typescript-eslint/no-unsafe-assignment": "warn",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-loss-of-precision": "warn",
"no-case-declarations": "warn",
"@typescript-eslint/no-unnecessary-condition": "warn",
"no-case-declarations": "error",
"prettier/prettier": "error",
"@typescript-eslint/semi": "error",
"no-mixed-spaces-and-tabs": "error"
"no-mixed-spaces-and-tabs": "error",
"require-await": "off",
"@typescript-eslint/require-await": "error"
},
"parser": "@typescript-eslint/parser",
"parserOptions": {

View File

@ -17,6 +17,5 @@ jobs:
node-version: ${{ matrix.version }}
- run: npm ci
- run: cp config.json.example config.json
- run: echo '{"version":"","buildLabel":"","matchmakingBuildId":""}' > static/data/buildConfig.json
- run: npm run build
- run: npm run lint

25
.github/workflows/docker.yml vendored Normal file
View File

@ -0,0 +1,25 @@
name: Build Docker image
on:
push:
branches:
- main
jobs:
docker:
if: github.repository == 'OpenWF/SpaceNinjaServer'
runs-on: ubuntu-latest
steps:
- name: Set up Docker buildx
uses: docker/setup-buildx-action@v3
- name: Log in to container registry
uses: docker/login-action@v3
with:
username: openwf
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64
push: true
tags: |
openwf/spaceninjaserver:latest
openwf/spaceninjaserver:${{ github.sha }}

5
.gitignore vendored
View File

@ -15,4 +15,7 @@ yarn.lock
/logs
# MongoDB VSCode extension playground scripts
/database_scripts
/database_scripts
# Default Docker directory
/docker-data

View File

@ -1,2 +1,3 @@
static/webui/libs/
*.html
*.md

19
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,19 @@
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug and Watch",
"runtimeArgs": ["-r", "tsconfig-paths/register", "-r", "ts-node/register", "--watch-path", "src"],
"args": ["${workspaceFolder}/src/index.ts"],
"console": "integratedTerminal"
}
]
}
//can use "console": "internalConsole" for VS Code's Debug Console. For that, forceConsole in logger.ts is needed to be true
//"internalConsoleOptions": "openOnSessionStart" can be useful then

View File

@ -1,5 +1,28 @@
FROM mongo as base
FROM node:18-alpine3.19
EXPOSE 27017
ENV APP_MONGODB_URL=mongodb://mongodb:27017/openWF
ENV APP_MY_ADDRESS=localhost
ENV APP_HTTP_PORT=80
ENV APP_HTTPS_PORT=443
ENV APP_AUTO_CREATE_ACCOUNT=true
ENV APP_SKIP_STORY_MODE_CHOICE=true
ENV APP_SKIP_TUTORIAL=true
ENV APP_SKIP_ALL_DIALOGUE=true
ENV APP_UNLOCK_ALL_SCANS=true
ENV APP_UNLOCK_ALL_MISSIONS=true
ENV APP_UNLOCK_ALL_QUESTS=true
ENV APP_COMPLETE_ALL_QUESTS=true
ENV APP_INFINITE_RESOURCES=true
ENV APP_UNLOCK_ALL_SHIP_FEATURES=true
ENV APP_UNLOCK_ALL_SHIP_DECORATIONS=true
ENV APP_UNLOCK_ALL_FLAVOUR_ITEMS=true
ENV APP_UNLOCK_ALL_SKINS=true
ENV APP_UNIVERSAL_POLARITY_EVERYWHERE=true
ENV APP_SPOOF_MASTERY_RANK=-1
CMD ["mongod"]
RUN apk add --no-cache bash sed wget jq
COPY . /app
WORKDIR /app
ENTRYPOINT ["/app/docker-entrypoint.sh"]

View File

@ -1,3 +1,14 @@
# Space Ninja Server
More information for the moment here: [https://discord.gg/PNNZ3asUuY](https://discord.gg/PNNZ3asUuY)
## Project Status
This project is in active development at <https://onlyg.it/OpenWF/SpaceNinjaServer>.
To get an idea of what functionality you can expect to be missing [have a look through the issues](https://onlyg.it/OpenWF/SpaceNinjaServer/issues?q=&type=all&state=open&labels=-4%2C-10&milestone=0&assignee=0&poster=). However, many things have been implemented and *should* work as expected. Please open an issue for anything where that's not the case and/or the server is reporting errors.
## config.json
- `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 ]`.

View File

@ -0,0 +1,26 @@
@echo off
echo Updating SpaceNinjaServer...
git config remote.origin.url https://openwf.io/SpaceNinjaServer.git
git fetch --prune
git stash
git reset --hard origin/main
if exist static\data\0\ (
echo Updating stripped assets...
cd static\data\0\
git pull
cd ..\..\..\
)
echo Updating dependencies...
call npm i --omit=dev
call npm run build
if %errorlevel% == 0 (
call npm run start
echo SpaceNinjaServer seems to have crashed.
)
:a
pause > nul
goto a

View File

@ -2,25 +2,46 @@
"mongodbUrl": "mongodb://127.0.0.1:27017/openWF",
"logger": {
"files": true,
"level": "trace",
"__valid_levels": "fatal, error, warn, info, http, debug, trace"
"level": "trace"
},
"myAddress": "localhost",
"httpPort": 80,
"httpsPort": 443,
"NRS": ["localhost"],
"administratorNames": [],
"autoCreateAccount": true,
"skipStoryModeChoice": true,
"skipTutorial": true,
"skipAllDialogue": true,
"unlockAllScans": true,
"unlockAllMissions": true,
"unlockAllQuests": true,
"completeAllQuests": false,
"infiniteResources": true,
"unlockAllShipFeatures": true,
"unlockAllShipDecorations": true,
"unlockAllFlavourItems": true,
"unlockAllSkins": true,
"universalPolarityEverywhere": true,
"spoofMasteryRank": -1
"skipTutorial": false,
"skipAllDialogue": false,
"unlockAllScans": false,
"unlockAllMissions": false,
"infiniteCredits": false,
"infinitePlatinum": false,
"infiniteEndo": false,
"infiniteRegalAya": false,
"infiniteHelminthMaterials": false,
"unlockAllShipFeatures": false,
"unlockAllShipDecorations": false,
"unlockAllFlavourItems": false,
"unlockAllSkins": false,
"unlockAllCapturaScenes": false,
"universalPolarityEverywhere": false,
"unlockDoubleCapacityPotatoesEverywhere": false,
"unlockExilusEverywhere": false,
"unlockArcanesEverywhere": false,
"noDailyStandingLimits": false,
"noArgonCrystalDecay": false,
"noVendorPurchaseLimits": true,
"instantResourceExtractorDrones": false,
"noDojoRoomBuildStage": false,
"fastDojoRoomDestruction": false,
"noDojoResearchCosts": false,
"noDojoResearchTime": false,
"fastClanAscension": false,
"spoofMasteryRank": -1,
"events": {
"creditBoost": false,
"affinityBoost": false,
"resourceBoost": false,
"starDays": true
}
}

View File

@ -1,24 +1,43 @@
version: "3.9"
services:
mongodb:
container_name: mongodb
image: mongodb
restart: always
build:
context: .
dockerfile: Dockerfile
target: base
spaceninjaserver:
# build: .
image: openwf/spaceninjaserver:latest
environment:
MONGO_INITDB_ROOT_USERNAME: ${DATABASE_USERNAME}
MONGO_INITDB_ROOT_PASSWORD: ${DATABASE_PASSWORD}
ports:
- ${DATABASE_PORT}:${DATABASE_PORT}
expose:
- "${DATABASE_PORT}"
networks:
- docker
APP_MONGODB_URL: mongodb://openwfagent:spaceninjaserver@mongodb:27017/
networks:
docker:
external: true
# Following environment variables are set to default image values.
# Uncomment to edit.
# APP_MY_ADDRESS: localhost
# APP_HTTP_PORT: 80
# APP_HTTPS_PORT: 443
# APP_AUTO_CREATE_ACCOUNT: true
# APP_SKIP_STORY_MODE_CHOICE: true
# APP_SKIP_TUTORIAL: true
# APP_SKIP_ALL_DIALOGUE: true
# APP_UNLOCK_ALL_SCANS: true
# APP_UNLOCK_ALL_MISSIONS: true
# APP_UNLOCK_ALL_QUESTS: true
# APP_COMPLETE_ALL_QUESTS: true
# APP_INFINITE_RESOURCES: true
# APP_UNLOCK_ALL_SHIP_FEATURES: true
# APP_UNLOCK_ALL_SHIP_DECORATIONS: true
# APP_UNLOCK_ALL_FLAVOUR_ITEMS: true
# APP_UNLOCK_ALL_SKINS: true
# APP_UNIVERSAL_POLARITY_EVERYWHERE: true
# APP_SPOOF_MASTERY_RANK: -1
volumes:
- ./docker-data/static:/app/static/data
- ./docker-data/logs:/app/logs
ports:
- 80:80
- 443:443
depends_on:
- mongodb
mongodb:
image: docker.io/library/mongo:8.0.0-noble
environment:
MONGO_INITDB_ROOT_USERNAME: openwfagent
MONGO_INITDB_ROOT_PASSWORD: spaceninjaserver
volumes:
- ./docker-data/database:/data/db

23
docker-entrypoint.sh Executable file
View File

@ -0,0 +1,23 @@
#!/bin/bash
set -e
# Set up the configuration file using environment variables.
echo '{
"logger": {
"files": true,
"level": "trace",
"__valid_levels": "fatal, error, warn, info, http, debug, trace"
}
}
' > config.json
for config in $(env | grep "APP_")
do
var=$(echo "${config}" | tr '[:upper:]' '[:lower:]' | sed 's/app_//g' | sed -E 's/_([a-z])/\U\1/g' | sed 's/=.*//g')
val=$(echo "${config}" | sed 's/.*=//g')
jq --arg variable "$var" --arg value "$val" '.[$variable] += try [$value|fromjson][] catch $value' config.json > config.tmp
mv config.tmp config.json
done
npm install
exec npm run dev

2212
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,30 +9,33 @@
"build": "tsc && copyfiles static/webui/** build",
"lint": "eslint --ext .ts .",
"lint:fix": "eslint --fix --ext .ts .",
"prettier": "prettier --write ."
"prettier": "prettier --write .",
"update-translations": "cd scripts && node update-translations.js"
},
"license": "GNU",
"dependencies": {
"@types/express": "^5",
"@types/morgan": "^1.9.9",
"copyfiles": "^2.4.1",
"express": "^5.0.0-beta.3",
"mongoose": "^8.4.5",
"warframe-public-export-plus": "^0.4.4",
"warframe-riven-info": "^0.1.1",
"winston": "^3.13.0",
"crc-32": "^1.2.2",
"express": "^5",
"json-with-bigint": "^3.2.1",
"mongoose": "^8.11.0",
"morgan": "^1.10.0",
"typescript": ">=5.5 <5.6.0",
"warframe-public-export-plus": "^0.5.48",
"warframe-riven-info": "^0.1.2",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0"
},
"devDependencies": {
"@types/express": "^4.17.20",
"@types/morgan": "^1.9.9",
"@typescript-eslint/eslint-plugin": "^7.14",
"@typescript-eslint/parser": "^7.14",
"@typescript-eslint/eslint-plugin": "^7.18",
"@typescript-eslint/parser": "^7.18",
"eslint": "^8.56.0",
"eslint-plugin-prettier": "^5.1.3",
"morgan": "^1.10.0",
"prettier": "^3.3.2",
"eslint-plugin-prettier": "^5.2.3",
"prettier": "^3.4.2",
"ts-node-dev": "^2.0.0",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.5"
"tsconfig-paths": "^4.2.0"
},
"engines": {
"node": ">=18.15.0",

View File

@ -0,0 +1,46 @@
// Based on https://onlyg.it/OpenWF/Translations/src/branch/main/update.php
// Converted via ChatGPT-4o
const fs = require("fs");
function extractStrings(content) {
const regex = /([a-zA-Z_]+): `([^`]*)`,/g;
let matches;
const strings = {};
while ((matches = regex.exec(content)) !== null) {
strings[matches[1]] = matches[2];
}
return strings;
}
const source = fs.readFileSync("../static/webui/translations/en.js", "utf8");
const sourceStrings = extractStrings(source);
const sourceLines = source.split("\n");
fs.readdirSync("../static/webui/translations").forEach(file => {
if (fs.lstatSync(`../static/webui/translations/${file}`).isFile() && file !== "en.js") {
const content = fs.readFileSync(`../static/webui/translations/${file}`, "utf8");
const targetStrings = extractStrings(content);
const contentLines = content.split("\n");
const fileHandle = fs.openSync(`../static/webui/translations/${file}`, "w");
fs.writeSync(fileHandle, contentLines[0] + "\n");
sourceLines.forEach(line => {
const strings = extractStrings(line);
if (Object.keys(strings).length > 0) {
Object.entries(strings).forEach(([key, value]) => {
if (targetStrings.hasOwnProperty(key)) {
fs.writeSync(fileHandle, ` ${key}: \`${targetStrings[key]}\`,\n`);
} else {
fs.writeSync(fileHandle, ` ${key}: \`[UNTRANSLATED] ${value}\`,\n`);
}
});
} else if (line.length) {
fs.writeSync(fileHandle, line + "\n");
}
});
fs.closeSync(fileHandle);
}
});

View File

@ -1,45 +1,44 @@
import express from "express";
import bodyParser from "body-parser";
import { unknownEndpointHandler } from "@/src/middleware/middleware";
import { requestLogger } from "@/src/middleware/morgenMiddleware";
import { errorHandler } from "@/src/middleware/errorHandler";
import { apiRouter } from "@/src/routes/api";
//import { testRouter } from "@/src/routes/test";
import { cacheRouter } from "@/src/routes/cache";
import bodyParser from "body-parser";
import { steamPacksController } from "@/src/controllers/misc/steamPacksController";
import { customRouter } from "@/src/routes/custom";
import { dynamicController } from "@/src/routes/dynamic";
import { payRouter } from "@/src/routes/pay";
import { statsRouter } from "@/src/routes/stats";
import { webuiRouter } from "@/src/routes/webui";
import { connectDatabase } from "@/src/services/mongoService";
import { registerLogFileCreationListener } from "@/src/utils/logger";
void registerLogFileCreationListener();
void connectDatabase();
const app = express();
app.use((req, _res, next) => {
// 38.5.0 introduced "ezip" for encrypted body blobs.
// The bootstrapper decrypts it for us but having an unsupported Content-Encoding here would still be an issue for Express, so removing it.
if (req.headers["content-encoding"] == "ezip") {
req.headers["content-encoding"] = undefined;
}
next();
});
app.use(bodyParser.raw());
app.use(express.json());
app.use(express.json({ limit: "4mb" }));
app.use(bodyParser.text());
app.use(requestLogger);
//app.use(requestLogger);
app.use("/api", apiRouter);
//app.use("/test", testRouter);
app.use("/", cacheRouter);
app.use("/custom", customRouter);
app.use("/dynamic", dynamicController);
app.use("/:id/dynamic", dynamicController);
app.post("/pay/steamPacks.php", steamPacksController);
app.use("/pay", payRouter);
app.use("/stats", statsRouter);
app.use("/", webuiRouter);
app.use(unknownEndpointHandler);
//app.use(errorHandler)
app.use(errorHandler);
export { app };

View File

@ -0,0 +1,11 @@
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const abandonLibraryDailyTaskController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
inventory.LibraryActiveDailyTaskInfo = undefined;
await inventory.save();
res.status(200).end();
};

View File

@ -0,0 +1,46 @@
import {
getDojoClient,
getGuildForRequestEx,
hasAccessToDojo,
hasGuildPermission,
removeDojoDeco,
removeDojoRoom
} from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
import { RequestHandler } from "express";
export const abortDojoComponentController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "GuildId LevelKeys");
const guild = await getGuildForRequestEx(req, inventory);
const request = JSON.parse(String(req.body)) as IAbortDojoComponentRequest;
if (
!hasAccessToDojo(inventory) ||
!(await hasGuildPermission(
guild,
accountId,
request.DecoId ? GuildPermission.Decorator : GuildPermission.Architect
))
) {
res.json({ DojoRequestStatus: -1 });
return;
}
if (request.DecoId) {
removeDojoDeco(guild, request.ComponentId, request.DecoId);
} else {
removeDojoRoom(guild, request.ComponentId);
}
await guild.save();
res.json(await getDojoClient(guild, 0, request.ComponentId));
};
interface IAbortDojoComponentRequest {
DecoType?: string;
ComponentId: string;
DecoId?: string;
}

View File

@ -0,0 +1,21 @@
import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, hasGuildPermission } from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
import { RequestHandler } from "express";
export const abortDojoComponentDestructionController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "GuildId LevelKeys");
const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Architect))) {
res.json({ DojoRequestStatus: -1 });
return;
}
const componentId = req.query.componentId as string;
guild.DojoComponents.id(componentId)!.DestructionTime = undefined;
await guild.save();
res.json(await getDojoClient(guild, 0, componentId));
};

View File

@ -0,0 +1,40 @@
import { toOid } from "@/src/helpers/inventoryHelpers";
import { createVeiledRivenFingerprint, 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";
export const activateRandomModController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const request = getJSONfromString<IActiveRandomModRequest>(String(req.body));
addMods(inventory, [
{
ItemType: request.ItemType,
ItemCount: -1
}
]);
const rivenType = getRandomElement(rivenRawToRealWeighted[request.ItemType]);
const fingerprint = createVeiledRivenFingerprint(ExportUpgrades[rivenType]);
const upgradeIndex =
inventory.Upgrades.push({
ItemType: rivenType,
UpgradeFingerprint: JSON.stringify(fingerprint)
}) - 1;
await inventory.save();
// For some reason, in this response, the UpgradeFingerprint is simply a nested object and not a string
res.json({
NewMod: {
UpgradeFingerprint: fingerprint,
ItemType: rivenType,
ItemId: toOid(inventory.Upgrades[upgradeIndex]._id)
}
});
};
interface IActiveRandomModRequest {
ItemType: string;
}

View File

@ -1,17 +1,25 @@
import { RequestHandler } from "express";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { IUpdateGlyphRequest } from "@/src/types/requestTypes";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory } from "@/src/services/inventoryService";
import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
const addFriendImageController: RequestHandler = async (req, res) => {
export const addFriendImageController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const json = getJSONfromString(String(req.body)) as IUpdateGlyphRequest;
const inventory = await getInventory(accountId);
inventory.ActiveAvatarImageType = json.AvatarImageType;
await inventory.save();
const json = getJSONfromString<IUpdateGlyphRequest>(String(req.body));
await Inventory.updateOne(
{
accountOwnerId: accountId
},
{
ActiveAvatarImageType: json.AvatarImageType
}
);
res.json({});
};
export { addFriendImageController };
interface IUpdateGlyphRequest {
AvatarImageType: string;
AvatarImage: string;
}

View File

@ -0,0 +1,77 @@
import { Guild, GuildMember } from "@/src/models/guildModel";
import { Account } from "@/src/models/loginModel";
import { fillInInventoryDataForGuildMember, hasGuildPermission } from "@/src/services/guildService";
import { createMessage } from "@/src/services/inboxService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService";
import { IOid } from "@/src/types/commonTypes";
import { GuildPermission, IGuildMemberClient } from "@/src/types/guildTypes";
import { RequestHandler } from "express";
import { ExportFlavour } from "warframe-public-export-plus";
export const addToGuildController: RequestHandler = async (req, res) => {
const payload = JSON.parse(String(req.body)) as IAddToGuildRequest;
const account = await Account.findOne({ DisplayName: payload.UserName });
if (!account) {
res.status(400).json("Username does not exist");
return;
}
const guild = (await Guild.findOne({ _id: payload.GuildId.$oid }, "Name"))!;
const senderAccount = await getAccountForRequest(req);
if (!(await hasGuildPermission(guild, senderAccount._id.toString(), GuildPermission.Recruiter))) {
res.status(400).json("Invalid permission");
}
if (
await GuildMember.exists({
accountId: account._id,
guildId: payload.GuildId.$oid
})
) {
res.status(400).json("User already invited to clan");
return;
}
await GuildMember.insertOne({
accountId: account._id,
guildId: payload.GuildId.$oid,
status: 2 // outgoing invite
});
const senderInventory = await getInventory(senderAccount._id.toString(), "ActiveAvatarImageType");
await createMessage(account._id.toString(), [
{
sndr: getSuffixedName(senderAccount),
msg: "/Lotus/Language/Menu/Mailbox_ClanInvite_Body",
arg: [
{
Key: "clan",
Tag: guild.Name
}
],
sub: "/Lotus/Language/Menu/Mailbox_ClanInvite_Title",
icon: ExportFlavour[senderInventory.ActiveAvatarImageType].icon,
contextInfo: payload.GuildId.$oid,
highPriority: true,
acceptAction: "GUILD_INVITE",
declineAction: "GUILD_INVITE",
hasAccountAction: true
}
]);
const member: IGuildMemberClient = {
_id: { $oid: account._id.toString() },
DisplayName: account.DisplayName,
Rank: 7,
Status: 2
};
await fillInInventoryDataForGuildMember(member);
res.json({ NewMember: member });
};
interface IAddToGuildRequest {
UserName: string;
GuildId: IOid;
}

View File

@ -0,0 +1,76 @@
import { RequestHandler } from "express";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory, addMods } from "@/src/services/inventoryService";
import { IOid } from "@/src/types/commonTypes";
export const arcaneCommonController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const json = getJSONfromString<IArcaneCommonRequest>(String(req.body));
const inventory = await getInventory(accountId);
const upgrade = inventory.Upgrades.id(json.arcane.ItemId.$oid);
if (json.newRank == -1) {
// Break down request?
if (!upgrade || !upgrade.UpgradeFingerprint) {
throw new Error(`Failed to find upgrade with OID ${json.arcane.ItemId.$oid}`);
}
// Remove Upgrade
inventory.Upgrades.pull({ _id: json.arcane.ItemId.$oid });
// Add RawUpgrades
const numRawUpgradesToGive = arcaneLevelCounts[(JSON.parse(upgrade.UpgradeFingerprint) as { lvl: number }).lvl];
addMods(inventory, [
{
ItemType: json.arcane.ItemType,
ItemCount: numRawUpgradesToGive
}
]);
res.json({ upgradeId: json.arcane.ItemId.$oid, numConsumed: numRawUpgradesToGive });
} else {
// Upgrade request?
let numConsumed = arcaneLevelCounts[json.newRank];
let upgradeId = json.arcane.ItemId.$oid;
if (upgrade) {
// Have an existing Upgrade item?
if (upgrade.UpgradeFingerprint) {
const existingLevel = (JSON.parse(upgrade.UpgradeFingerprint) as { lvl: number }).lvl;
numConsumed -= arcaneLevelCounts[existingLevel];
}
upgrade.UpgradeFingerprint = JSON.stringify({ lvl: json.newRank });
} else {
const newLength = inventory.Upgrades.push({
ItemType: json.arcane.ItemType,
UpgradeFingerprint: JSON.stringify({ lvl: json.newRank })
});
upgradeId = inventory.Upgrades[newLength - 1]._id.toString();
}
// Remove RawUpgrades
addMods(inventory, [
{
ItemType: json.arcane.ItemType,
ItemCount: numConsumed * -1
}
]);
res.json({ newLevel: json.newRank, numConsumed, upgradeId });
}
await inventory.save();
};
const arcaneLevelCounts = [0, 3, 6, 10, 15, 21];
interface IArcaneCommonRequest {
arcane: {
ItemType: string;
ItemId: IOid;
FromSKU: boolean;
UpgradeFingerprint: string;
PendingRerollFingerprint: string;
ItemCount: number;
LastAdded: IOid;
};
newRank: number;
}

View File

@ -0,0 +1,51 @@
import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { addMiscItems, getInventory } from "@/src/services/inventoryService";
import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
import { colorToShard, combineColors, shardToColor } from "@/src/helpers/shardHelper";
export const archonFusionController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const request = JSON.parse(String(req.body)) as IArchonFusionRequest;
const inventory = await getInventory(accountId);
request.Consumed.forEach(x => {
x.ItemCount *= -1;
});
addMiscItems(inventory, request.Consumed);
const newArchons: IMiscItem[] = [];
switch (request.FusionType) {
case "AFT_ASCENT":
newArchons.push({
ItemType: request.Consumed[0].ItemType + "Mythic",
ItemCount: 1
});
break;
case "AFT_COALESCENT":
newArchons.push({
ItemType:
colorToShard[
combineColors(
shardToColor[request.Consumed[0].ItemType],
shardToColor[request.Consumed[1].ItemType]
)
],
ItemCount: 1
});
break;
default:
throw new Error(`unknown archon fusion type: ${request.FusionType}`);
}
addMiscItems(inventory, newArchons);
await inventory.save();
res.json({
NewArchons: newArchons
});
};
interface IArchonFusionRequest {
Consumed: IMiscItem[];
FusionType: string;
StatResultType: "SRT_NEW_STAT"; // ???
}

View File

@ -0,0 +1,124 @@
import { 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 { RequestHandler } from "express";
import { ExportBoosterPacks, ExportUpgrades, TRarity } from "warframe-public-export-plus";
export const artifactTransmutationController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const payload = JSON.parse(String(req.body)) as IArtifactTransmutationRequest;
inventory.RegularCredits -= payload.Cost;
inventory.FusionPoints -= payload.FusionPointCost;
if (payload.RivenTransmute) {
addMiscItems(inventory, [
{
ItemType: "/Lotus/Types/Gameplay/Eidolon/Resources/SentientSecretItem",
ItemCount: -1
}
]);
payload.Consumed.forEach(upgrade => {
inventory.Upgrades.pull({ _id: upgrade.ItemId.$oid });
});
const rawRivenType = getRandomRawRivenType();
const rivenType = getRandomElement(rivenRawToRealWeighted[rawRivenType]);
const fingerprint = createVeiledRivenFingerprint(ExportUpgrades[rivenType]);
const upgradeIndex =
inventory.Upgrades.push({
ItemType: rivenType,
UpgradeFingerprint: JSON.stringify(fingerprint)
}) - 1;
await inventory.save();
res.json({
NewMods: [
{
ItemId: toOid(inventory.Upgrades[upgradeIndex]._id),
ItemType: rivenType,
UpgradeFingerprint: fingerprint
}
]
});
} else {
const counts: Record<TRarity, number> = {
COMMON: 0,
UNCOMMON: 0,
RARE: 0,
LEGENDARY: 0
};
payload.Consumed.forEach(upgrade => {
const meta = ExportUpgrades[upgrade.ItemType];
counts[meta.rarity] += upgrade.ItemCount;
addMods(inventory, [
{
ItemType: upgrade.ItemType,
ItemCount: upgrade.ItemCount * -1
}
]);
});
// Based on the table on https://wiki.warframe.com/w/Transmutation
const weights: Record<TRarity, number> = {
COMMON: counts.COMMON * 95 + counts.UNCOMMON * 15 + counts.RARE * 4,
UNCOMMON: counts.COMMON * 4 + counts.UNCOMMON * 80 + counts.RARE * 10,
RARE: counts.COMMON * 1 + counts.UNCOMMON * 5 + counts.RARE * 50,
LEGENDARY: 0
};
const options: { uniqueName: string; rarity: TRarity }[] = [];
Object.entries(ExportUpgrades).forEach(([uniqueName, upgrade]) => {
if (upgrade.canBeTransmutation) {
options.push({ uniqueName, rarity: upgrade.rarity });
}
});
const newModType = getRandomWeightedReward(options, weights)!.uniqueName;
addMods(inventory, [
{
ItemType: newModType,
ItemCount: 1
}
]);
await inventory.save();
res.json({
NewMods: [
{
ItemType: newModType,
ItemCount: 1
}
]
});
}
};
const getRandomRawRivenType = (): string => {
const pack = ExportBoosterPacks["/Lotus/Types/BoosterPacks/CalendarRivenPack"];
return getRandomWeightedRewardUc(pack.components, pack.rarityWeightsPerRoll[0])!.Item;
};
interface IArtifactTransmutationRequest {
Upgrade: IAgnosticUpgradeClient;
LevelDiff: number;
Consumed: IAgnosticUpgradeClient[];
Cost: number;
FusionPointCost: number;
RivenTransmute?: boolean;
}
interface IAgnosticUpgradeClient {
ItemType: string;
ItemId: IOid;
FromSKU: boolean;
UpgradeFingerprint: string;
PendingRerollFingerprint: string;
ItemCount: number;
LastAdded: IOid;
}

View File

@ -1,22 +1,70 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { upgradeMod } from "@/src/services/inventoryService";
import { IArtifactsRequest } from "@/src/types/requestTypes";
import { RequestHandler } from "express";
import { IInventoryClient, IUpgradeClient } from "@/src/types/inventoryTypes/inventoryTypes";
import { addMods, getInventory } from "@/src/services/inventoryService";
import { config } from "@/src/services/configService";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
const artifactsController: RequestHandler = async (req, res) => {
export const artifactsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const artifactsData = getJSONfromString<IArtifactsRequest>(String(req.body));
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call
const artifactsData = getJSONfromString(req.body.toString()) as IArtifactsRequest;
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
const upgradeModId = await upgradeMod(artifactsData, accountId);
res.send(upgradeModId);
} catch (err) {
console.error("Error parsing JSON data:", err);
const { Upgrade, LevelDiff, Cost, FusionPointCost } = artifactsData;
const inventory = await getInventory(accountId);
const { Upgrades } = inventory;
const { ItemType, UpgradeFingerprint, ItemId } = Upgrade;
const safeUpgradeFingerprint = UpgradeFingerprint || '{"lvl":0}';
const parsedUpgradeFingerprint = JSON.parse(safeUpgradeFingerprint) as { lvl: number };
parsedUpgradeFingerprint.lvl += LevelDiff;
const stringifiedUpgradeFingerprint = JSON.stringify(parsedUpgradeFingerprint);
let itemIndex = Upgrades.findIndex(upgrade => upgrade._id.equals(ItemId.$oid));
if (itemIndex !== -1) {
Upgrades[itemIndex].UpgradeFingerprint = stringifiedUpgradeFingerprint;
inventory.markModified(`Upgrades.${itemIndex}.UpgradeFingerprint`);
} else {
itemIndex =
Upgrades.push({
UpgradeFingerprint: stringifiedUpgradeFingerprint,
ItemType
}) - 1;
addMods(inventory, [{ ItemType, ItemCount: -1 }]);
}
if (!config.infiniteCredits) {
inventory.RegularCredits -= Cost;
}
if (!config.infiniteEndo) {
inventory.FusionPoints -= FusionPointCost;
}
if (artifactsData.LegendaryFusion) {
addMods(inventory, [
{
ItemType: "/Lotus/Upgrades/Mods/Fusers/LegendaryModFuser",
ItemCount: -1
}
]);
}
const changedInventory = await inventory.save();
const itemId = changedInventory.toJSON<IInventoryClient>().Upgrades[itemIndex].ItemId.$oid;
if (!itemId) {
throw new Error("Item Id not found in upgradeMod");
}
res.send(itemId);
};
export { artifactsController };
interface IArtifactsRequest {
Upgrade: IUpgradeClient;
LevelDiff: number;
Cost: number;
FusionPointCost: number;
LegendaryFusion?: boolean;
}

View File

@ -0,0 +1,97 @@
import { RequestHandler } from "express";
import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, hasGuildPermission } from "@/src/services/guildService";
import { logger } from "@/src/utils/logger";
import { GuildPermission, IDojoComponentDatabase } from "@/src/types/guildTypes";
import { Types } from "mongoose";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory } from "@/src/services/inventoryService";
export const changeDojoRootController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "GuildId LevelKeys");
const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Architect))) {
res.json({ DojoRequestStatus: -1 });
return;
}
const idToNode: Record<string, INode> = {};
guild.DojoComponents.forEach(x => {
idToNode[x._id.toString()] = {
component: x,
parent: undefined,
children: []
};
});
let oldRoot: INode | undefined;
guild.DojoComponents.forEach(x => {
const node = idToNode[x._id.toString()];
if (x.pi) {
idToNode[x.pi.toString()].children.push(node);
node.parent = idToNode[x.pi.toString()];
} else {
oldRoot = node;
}
});
logger.debug("Old tree:\n" + treeToString(oldRoot!));
const newRoot = idToNode[req.query.newRoot as string];
recursivelyTurnParentsIntoChildren(newRoot);
newRoot.component.pi = undefined;
newRoot.component.op = undefined;
newRoot.component.pp = undefined;
newRoot.parent = undefined;
// Don't even ask me why this is needed because I don't know either
const stack: INode[] = [newRoot];
let i = 0;
const idMap: Record<string, Types.ObjectId> = {};
while (stack.length != 0) {
const top = stack.shift()!;
idMap[top.component._id.toString()] = new Types.ObjectId(
(++i).toString(16).padStart(8, "0") + top.component._id.toString().substr(8)
);
top.children.forEach(x => stack.push(x));
}
guild.DojoComponents.forEach(x => {
x._id = idMap[x._id.toString()];
if (x.pi) {
x.pi = idMap[x.pi.toString()];
}
});
logger.debug("New tree:\n" + treeToString(newRoot));
await guild.save();
res.json(await getDojoClient(guild, 0));
};
interface INode {
component: IDojoComponentDatabase;
parent: INode | undefined;
children: INode[];
}
const treeToString = (root: INode, depth: number = 0): string => {
let str = " ".repeat(depth * 4) + root.component.pf + " (" + root.component._id.toString() + ")\n";
root.children.forEach(x => {
str += treeToString(x, depth + 1);
});
return str;
};
const recursivelyTurnParentsIntoChildren = (node: INode): void => {
if (node.parent!.parent) {
recursivelyTurnParentsIntoChildren(node.parent!);
}
node.parent!.component.pi = node.component._id;
node.parent!.component.op = node.component.pp;
node.parent!.component.pp = node.component.op;
node.parent!.parent = node;
node.parent!.children.splice(node.parent!.children.indexOf(node), 1);
node.children.push(node.parent!);
};

View File

@ -0,0 +1,38 @@
import { GuildMember } from "@/src/models/guildModel";
import { getGuildForRequest, hasGuildPermissionEx } from "@/src/services/guildService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
import { RequestHandler } from "express";
export const changeGuildRankController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const member = (await GuildMember.findOne({
accountId: accountId,
guildId: req.query.guildId as string
}))!;
const newRank: number = parseInt(req.query.rankChange as string);
const guild = await getGuildForRequest(req);
if (newRank < member.rank || !hasGuildPermissionEx(guild, member, GuildPermission.Promoter)) {
res.status(400).json("Invalid permission");
return;
}
const target = (await GuildMember.findOne({
guildId: req.query.guildId as string,
accountId: req.query.targetId as string
}))!;
target.rank = parseInt(req.query.rankChange as string);
await target.save();
if (newRank == 0) {
// If we just promoted someone else to Founding Warlord, we need to demote ourselves to Warlord.
member.rank = 1;
await member.save();
}
res.json({
_id: req.query.targetId as string,
Rank: newRank
});
};

View File

@ -7,82 +7,120 @@ import { getRecipe } from "@/src/services/itemDataService";
import { IOid } from "@/src/types/commonTypes";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory, updateCurrency, addItem, addMiscItems, addRecipes } from "@/src/services/inventoryService";
import {
getInventory,
updateCurrency,
addItem,
addMiscItems,
addRecipes,
occupySlot
} from "@/src/services/inventoryService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { IMiscItem, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
export interface IClaimCompletedRecipeRequest {
RecipeIds: IOid[];
}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const claimCompletedRecipeController: RequestHandler = async (req, res) => {
const claimCompletedRecipeRequest = getJSONfromString(String(req.body)) as IClaimCompletedRecipeRequest;
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 pendingRecipe = inventory.PendingRecipes.find(
recipe => recipe._id?.toString() === claimCompletedRecipeRequest.RecipeIds[0].$oid
);
const pendingRecipe = inventory.PendingRecipes.id(claimCompletedRecipeRequest.RecipeIds[0].$oid);
if (!pendingRecipe) {
logger.error(`no pending recipe found with id ${claimCompletedRecipeRequest.RecipeIds[0].$oid}`);
throw new Error(`no pending recipe found with id ${claimCompletedRecipeRequest.RecipeIds[0].$oid}`);
}
//check recipe is indeed ready to be completed
// if (pendingRecipe.CompletionDate > new Date()) {
// logger.error(`recipe ${pendingRecipe._id} is not ready to be completed`);
// throw new Error(`recipe ${pendingRecipe._id} is not ready to be completed`);
// }
inventory.PendingRecipes.pull(pendingRecipe._id);
await inventory.save();
const recipe = getRecipe(pendingRecipe.ItemType);
if (!recipe) {
logger.error(`no completed item found for recipe ${pendingRecipe._id}`);
throw new Error(`no completed item found for recipe ${pendingRecipe._id}`);
throw new Error(`no completed item found for recipe ${pendingRecipe._id.toString()}`);
}
if (req.query.cancel) {
const currencyChanges = await updateCurrency(recipe.buildPrice * -1, false, accountId);
const inventoryChanges: IInventoryChanges = {
...updateCurrency(inventory, recipe.buildPrice * -1, false)
};
const inventory = await getInventory(accountId);
addMiscItems(inventory, recipe.ingredients);
await inventory.save();
const nonMiscItemIngredients = 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>());
nonMiscItemIngredients.add(item.ItemType);
// Not a bug: In the specific case of cancelling a recipe, InventoryChanges are expected to be the root.
res.json({
...currencyChanges,
MiscItems: recipe.ingredients
occupySlot(inventory, InventorySlot.WEAPONS, false);
inventoryChanges.WeaponBin ??= { Slots: 0 };
inventoryChanges.WeaponBin.Slots -= 1;
});
}
}
const miscItemChanges: IMiscItem[] = [];
recipe.ingredients.forEach(ingredient => {
if (!nonMiscItemIngredients.has(ingredient.ItemType)) {
miscItemChanges.push(ingredient);
}
});
addMiscItems(inventory, miscItemChanges);
inventoryChanges.MiscItems = miscItemChanges;
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 });
if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") {
inventory.PendingSpectreLoadouts ??= [];
inventory.SpectreLoadouts ??= [];
const pendingLoadoutIndex = inventory.PendingSpectreLoadouts.findIndex(
x => x.ItemType == recipe.resultType
);
if (pendingLoadoutIndex != -1) {
const loadoutIndex = inventory.SpectreLoadouts.findIndex(x => x.ItemType == recipe.resultType);
if (loadoutIndex != -1) {
inventory.SpectreLoadouts.splice(loadoutIndex, 1);
}
logger.debug(
"moving spectre loadout from pending to active",
inventory.toJSON().PendingSpectreLoadouts![pendingLoadoutIndex]
);
inventory.SpectreLoadouts.push(inventory.PendingSpectreLoadouts[pendingLoadoutIndex]);
inventory.PendingSpectreLoadouts.splice(pendingLoadoutIndex, 1);
}
}
let InventoryChanges = {};
if (recipe.consumeOnUse) {
const recipeChanges = [
addRecipes(inventory, [
{
ItemType: pendingRecipe.ItemType,
ItemCount: -1
}
];
InventoryChanges = { ...InventoryChanges, Recipes: recipeChanges };
const inventory = await getInventory(accountId);
addRecipes(inventory, recipeChanges);
await inventory.save();
]);
}
if (req.query.rush) {
InventoryChanges = {
...InventoryChanges,
...(await updateCurrency(recipe.skipBuildTimePrice, true, accountId))
...updateCurrency(inventory, recipe.skipBuildTimePrice, true)
};
}
res.json({
InventoryChanges: {
...InventoryChanges,
...(await addItem(accountId, recipe.resultType, recipe.num)).InventoryChanges
}
});
InventoryChanges = {
...InventoryChanges,
...(await addItem(inventory, recipe.resultType, recipe.num, false))
};
await inventory.save();
res.json({ InventoryChanges });
}
};

View File

@ -0,0 +1,31 @@
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const claimLibraryDailyTaskRewardController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const rewardQuantity = inventory.LibraryActiveDailyTaskInfo!.RewardQuantity;
const rewardStanding = inventory.LibraryActiveDailyTaskInfo!.RewardStanding;
inventory.LibraryActiveDailyTaskInfo = undefined;
inventory.LibraryAvailableDailyTaskInfo = undefined;
let syndicate = inventory.Affiliations.find(x => x.Tag == "LibrarySyndicate");
if (!syndicate) {
syndicate = inventory.Affiliations[inventory.Affiliations.push({ Tag: "LibrarySyndicate", Standing: 0 }) - 1];
}
syndicate.Standing += rewardStanding;
inventory.FusionPoints += 80 * rewardQuantity;
await inventory.save();
res.json({
RewardItem: "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/RareFusionBundle",
RewardQuantity: rewardQuantity,
StandingAwarded: rewardStanding,
InventoryChanges: {
FusionPoints: 80 * rewardQuantity
}
});
};

View File

@ -0,0 +1,23 @@
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const clearDialogueHistoryController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const request = JSON.parse(String(req.body)) as IClearDialogueRequest;
if (inventory.DialogueHistory && inventory.DialogueHistory.Dialogues) {
for (const dialogueName of request.Dialogues) {
const index = inventory.DialogueHistory.Dialogues.findIndex(x => x.DialogueName == dialogueName);
if (index != -1) {
inventory.DialogueHistory.Dialogues.splice(index, 1);
}
}
}
await inventory.save();
res.end();
};
interface IClearDialogueRequest {
Dialogues: string[];
}

View File

@ -0,0 +1,45 @@
import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { addMiscItems, getInventory, updateCurrency } from "@/src/services/inventoryService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { createUnveiledRivenFingerprint } from "@/src/helpers/rivenHelper";
import { ExportUpgrades } from "warframe-public-export-plus";
export const completeRandomModChallengeController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const request = getJSONfromString<ICompleteRandomModChallengeRequest>(String(req.body));
let inventoryChanges: IInventoryChanges = {};
// Remove 20 plat or riven cipher
if ((req.query.p as string) == "1") {
inventoryChanges = { ...updateCurrency(inventory, 20, true) };
} else {
const miscItemChanges: IMiscItem[] = [
{
ItemType: "/Lotus/Types/Items/MiscItems/RivenIdentifier",
ItemCount: -1
}
];
addMiscItems(inventory, miscItemChanges);
inventoryChanges.MiscItems = miscItemChanges;
}
// Update riven fingerprint to a randomised unveiled state
const upgrade = inventory.Upgrades.id(request.ItemId)!;
const meta = ExportUpgrades[upgrade.ItemType];
upgrade.UpgradeFingerprint = JSON.stringify(createUnveiledRivenFingerprint(meta));
await inventory.save();
res.json({
InventoryChanges: inventoryChanges,
Fingerprint: upgrade.UpgradeFingerprint
});
};
interface ICompleteRandomModChallengeRequest {
ItemId: string;
}

View File

@ -0,0 +1,46 @@
import { Guild, GuildMember } from "@/src/models/guildModel";
import { getGuildClient, updateInventoryForConfirmedGuildJoin } from "@/src/services/guildService";
import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService";
import { RequestHandler } from "express";
import { Types } from "mongoose";
export const confirmGuildInvitationController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req);
const guildMember = await GuildMember.findOne({
accountId: account._id,
guildId: req.query.clanId as string
});
if (guildMember) {
guildMember.status = 0;
await guildMember.save();
await updateInventoryForConfirmedGuildJoin(
account._id.toString(),
new Types.ObjectId(req.query.clanId as string)
);
const guild = (await Guild.findOne({ _id: req.query.clanId as string }))!;
guild.RosterActivity ??= [];
guild.RosterActivity.push({
dateTime: new Date(),
entryType: 6,
details: getSuffixedName(account)
});
await guild.save();
res.json({
...(await getGuildClient(guild, account._id.toString())),
InventoryChanges: {
Recipes: [
{
ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint",
ItemCount: 1
}
]
}
});
} else {
res.end();
}
};

View File

@ -0,0 +1,104 @@
import { toMongoDate } from "@/src/helpers/inventoryHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Guild, GuildMember } from "@/src/models/guildModel";
import { config } from "@/src/services/configService";
import { createMessage } from "@/src/services/inboxService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
import { Types } from "mongoose";
export const contributeGuildClassController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const payload = getJSONfromString<IContributeGuildClassRequest>(String(req.body));
const guild = (await Guild.findOne({ _id: payload.GuildId }))!;
// First contributor initiates ceremony and locks the pending class.
if (!guild.CeremonyContributors) {
guild.CeremonyContributors = [];
guild.CeremonyClass = guildXpToClass(guild.XP);
guild.CeremonyEndo = 0;
for (let i = guild.Class; i != guild.CeremonyClass; ++i) {
guild.CeremonyEndo += (i + 1) * 1000;
}
guild.ClassChanges ??= [];
guild.ClassChanges.push({
dateTime: new Date(),
entryType: 13,
details: guild.CeremonyClass
});
}
guild.CeremonyContributors.push(new Types.ObjectId(accountId));
// Once required contributor count is hit, the class is committed and there's 72 hours to claim endo.
if (guild.CeremonyContributors.length == payload.RequiredContributors) {
guild.Class = guild.CeremonyClass!;
guild.CeremonyClass = undefined;
guild.CeremonyResetDate = new Date(Date.now() + (config.fastClanAscension ? 5_000 : 72 * 3600_000));
if (!config.fastClanAscension) {
// Send message to all active guild members
const members = await GuildMember.find({ guildId: payload.GuildId, status: 0 }, "accountId");
for (const member of members) {
// somewhat unfaithful as on live the "msg" is not a loctag, but since we don't have the string, we'll let the client fill it in with "arg".
await createMessage(member.accountId.toString(), [
{
sndr: guild.Name,
msg: "/Lotus/Language/Clan/Clan_AscensionCeremonyInProgressDetails",
arg: [
{
Key: "RESETDATE",
Tag:
guild.CeremonyResetDate.getUTCMonth() +
"/" +
guild.CeremonyResetDate.getUTCDate() +
"/" +
(guild.CeremonyResetDate.getUTCFullYear() % 100) +
" " +
guild.CeremonyResetDate.getUTCHours().toString().padStart(2, "0") +
":" +
guild.CeremonyResetDate.getUTCMinutes().toString().padStart(2, "0")
}
],
sub: "/Lotus/Language/Clan/Clan_AscensionCeremonyInProgress",
icon: "/Lotus/Interface/Graphics/ClanTileImages/ClanEnterDojo.png",
highPriority: true
}
]);
}
}
}
await guild.save();
// Either way, endo is given to the contributor.
const inventory = await getInventory(accountId, "FusionPoints");
inventory.FusionPoints += guild.CeremonyEndo!;
await inventory.save();
res.json({
NumContributors: guild.CeremonyContributors.length,
FusionPointReward: guild.CeremonyEndo,
Class: guild.Class,
CeremonyResetDate: guild.CeremonyResetDate ? toMongoDate(guild.CeremonyResetDate) : undefined
});
};
interface IContributeGuildClassRequest {
GuildId: string;
RequiredContributors: number;
}
const guildXpToClass = (xp: number): number => {
const cummXp = [
0, 11000, 34000, 69000, 114000, 168000, 231000, 302000, 381000, 68000, 563000, 665000, 774000, 891000
];
let highest = 0;
for (let i = 0; i != cummXp.length; ++i) {
if (xp < cummXp[i]) {
break;
}
highest = i;
}
return highest;
};

View File

@ -0,0 +1,155 @@
import { TGuildDatabaseDocument } from "@/src/models/guildModel";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import {
getDojoClient,
getGuildForRequestEx,
hasAccessToDojo,
processDojoBuildMaterialsGathered,
scaleRequiredCount,
setDojoRoomLogFunded
} from "@/src/services/guildService";
import { addMiscItems, getInventory, updateCurrency } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IDojoContributable } from "@/src/types/guildTypes";
import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { RequestHandler } from "express";
import { ExportDojoRecipes, IDojoBuild } from "warframe-public-export-plus";
interface IContributeToDojoComponentRequest {
ComponentId: string;
DecoId?: string;
DecoType?: string;
IngredientContributions: IMiscItem[];
RegularCredits: number;
VaultIngredientContributions: IMiscItem[];
VaultCredits: number;
}
export const contributeToDojoComponentController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
// Any clan member should have permission to contribute although notably permission is denied if they have not crafted the dojo key and were simply invited in.
if (!hasAccessToDojo(inventory)) {
res.json({ DojoRequestStatus: -1 });
return;
}
const guild = await getGuildForRequestEx(req, inventory);
const request = JSON.parse(String(req.body)) as IContributeToDojoComponentRequest;
const component = guild.DojoComponents.id(request.ComponentId)!;
const inventoryChanges: IInventoryChanges = {};
if (!component.CompletionTime) {
// Room is in "Collecting Materials" state
if (request.DecoId) {
throw new Error("attempt to contribute to a deco in an unfinished room?!");
}
const meta = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == component.pf)!;
processContribution(guild, request, inventory, inventoryChanges, meta, component);
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (component.CompletionTime) {
setDojoRoomLogFunded(guild, component);
}
} else {
// Room is past "Collecting Materials"
if (request.DecoId) {
const deco = component.Decos!.find(x => x._id.equals(request.DecoId))!;
const meta = Object.values(ExportDojoRecipes.decos).find(x => x.resultType == deco.Type)!;
processContribution(guild, request, inventory, inventoryChanges, meta, deco);
}
}
await guild.save();
await inventory.save();
res.json({
...(await getDojoClient(guild, 0, component._id)),
InventoryChanges: inventoryChanges
});
};
const processContribution = (
guild: TGuildDatabaseDocument,
request: IContributeToDojoComponentRequest,
inventory: TInventoryDatabaseDocument,
inventoryChanges: IInventoryChanges,
meta: IDojoBuild,
component: IDojoContributable
): void => {
component.RegularCredits ??= 0;
if (request.RegularCredits) {
component.RegularCredits += request.RegularCredits;
inventoryChanges.RegularCredits = -request.RegularCredits;
updateCurrency(inventory, request.RegularCredits, false);
}
if (request.VaultCredits) {
component.RegularCredits += request.VaultCredits;
guild.VaultRegularCredits! -= request.VaultCredits;
}
if (component.RegularCredits > scaleRequiredCount(meta.price)) {
guild.VaultRegularCredits ??= 0;
guild.VaultRegularCredits += component.RegularCredits - scaleRequiredCount(meta.price);
component.RegularCredits = scaleRequiredCount(meta.price);
}
component.MiscItems ??= [];
if (request.VaultIngredientContributions.length) {
for (const ingredientContribution of request.VaultIngredientContributions) {
const componentMiscItem = component.MiscItems.find(x => x.ItemType == ingredientContribution.ItemType);
if (componentMiscItem) {
const ingredientMeta = meta.ingredients.find(x => x.ItemType == ingredientContribution.ItemType)!;
if (
componentMiscItem.ItemCount + ingredientContribution.ItemCount >
scaleRequiredCount(ingredientMeta.ItemCount)
) {
ingredientContribution.ItemCount =
scaleRequiredCount(ingredientMeta.ItemCount) - componentMiscItem.ItemCount;
}
componentMiscItem.ItemCount += ingredientContribution.ItemCount;
} else {
component.MiscItems.push(ingredientContribution);
}
const vaultMiscItem = guild.VaultMiscItems!.find(x => x.ItemType == ingredientContribution.ItemType)!;
vaultMiscItem.ItemCount -= ingredientContribution.ItemCount;
}
}
if (request.IngredientContributions.length) {
const miscItemChanges: IMiscItem[] = [];
for (const ingredientContribution of request.IngredientContributions) {
const componentMiscItem = component.MiscItems.find(x => x.ItemType == ingredientContribution.ItemType);
if (componentMiscItem) {
const ingredientMeta = meta.ingredients.find(x => x.ItemType == ingredientContribution.ItemType)!;
if (
componentMiscItem.ItemCount + ingredientContribution.ItemCount >
scaleRequiredCount(ingredientMeta.ItemCount)
) {
ingredientContribution.ItemCount =
scaleRequiredCount(ingredientMeta.ItemCount) - componentMiscItem.ItemCount;
}
componentMiscItem.ItemCount += ingredientContribution.ItemCount;
} else {
component.MiscItems.push(ingredientContribution);
}
miscItemChanges.push({
ItemType: ingredientContribution.ItemType,
ItemCount: ingredientContribution.ItemCount * -1
});
}
addMiscItems(inventory, miscItemChanges);
inventoryChanges.MiscItems = miscItemChanges;
}
if (component.RegularCredits >= scaleRequiredCount(meta.price)) {
let fullyFunded = true;
for (const ingredient of meta.ingredients) {
const componentMiscItem = component.MiscItems.find(x => x.ItemType == ingredient.ItemType);
if (!componentMiscItem || componentMiscItem.ItemCount < scaleRequiredCount(ingredient.ItemCount)) {
fullyFunded = false;
break;
}
}
if (fullyFunded) {
component.CompletionTime = new Date(Date.now() + meta.time * 1000);
processDojoBuildMaterialsGathered(guild, meta);
}
}
};

View File

@ -0,0 +1,49 @@
import { getGuildForRequestEx } from "@/src/services/guildService";
import { addFusionTreasures, addMiscItems, addShipDecorations, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IFusionTreasure, IMiscItem, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express";
export const contributeToVaultController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const guild = await getGuildForRequestEx(req, inventory);
const request = JSON.parse(String(req.body)) as IContributeToVaultRequest;
if (request.RegularCredits) {
guild.VaultRegularCredits ??= 0;
guild.VaultRegularCredits += request.RegularCredits;
}
if (request.MiscItems.length) {
guild.VaultMiscItems ??= [];
for (const item of request.MiscItems) {
guild.VaultMiscItems.push(item);
addMiscItems(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]);
}
}
if (request.ShipDecorations.length) {
guild.VaultShipDecorations ??= [];
for (const item of request.ShipDecorations) {
guild.VaultShipDecorations.push(item);
addShipDecorations(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]);
}
}
if (request.FusionTreasures.length) {
guild.VaultFusionTreasures ??= [];
for (const item of request.FusionTreasures) {
guild.VaultFusionTreasures.push(item);
addFusionTreasures(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]);
}
}
await guild.save();
await inventory.save();
res.end();
};
interface IContributeToVaultRequest {
RegularCredits: number;
MiscItems: IMiscItem[];
ShipDecorations: ITypeCount[];
FusionTreasures: IFusionTreasure[];
}

View File

@ -1,38 +1,36 @@
import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
import { Guild } from "@/src/models/guildModel";
import { ICreateGuildRequest } from "@/src/types/guildTypes";
import { Guild, GuildMember } from "@/src/models/guildModel";
import {
createUniqueClanName,
getGuildClient,
updateInventoryForConfirmedGuildJoin
} from "@/src/services/guildService";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
const createGuildController: RequestHandler = async (req, res) => {
export const createGuildController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const payload = getJSONfromString(String(req.body)) as ICreateGuildRequest;
const payload = getJSONfromString<ICreateGuildRequest>(String(req.body));
// Create guild on database
const guild = new Guild({
Name: payload.guildName
Name: await createUniqueClanName(payload.guildName)
});
await guild.save();
// Update inventory
const inventory = await Inventory.findOne({ accountOwnerId: accountId });
if (inventory) {
// Set GuildId
inventory.GuildId = guild._id;
// Create guild member on database
await GuildMember.insertOne({
accountId: accountId,
guildId: guild._id,
status: 0,
rank: 0
});
// Give clan key (TODO: This should only be a blueprint)
inventory.LevelKeys ??= [];
inventory.LevelKeys.push({
ItemType: "/Lotus/Types/Keys/DojoKey",
ItemCount: 1
});
await updateInventoryForConfirmedGuildJoin(accountId, guild._id);
await inventory.save();
}
res.json(guild);
res.json(await getGuildClient(guild, accountId));
};
export { createGuildController };
interface ICreateGuildRequest {
guildName: string;
}

View File

@ -0,0 +1,27 @@
import { RequestHandler } from "express";
import { config } from "@/src/services/configService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory } from "@/src/services/inventoryService";
export const creditsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "RegularCredits TradesRemaining PremiumCreditsFree PremiumCredits");
const response = {
RegularCredits: inventory.RegularCredits,
TradesRemaining: inventory.TradesRemaining,
PremiumCreditsFree: inventory.PremiumCreditsFree,
PremiumCredits: inventory.PremiumCredits
};
if (config.infiniteCredits) {
response.RegularCredits = 999999999;
}
if (config.infinitePlatinum) {
response.PremiumCreditsFree = 0;
response.PremiumCredits = 999999999;
}
res.json(response);
};

View File

@ -0,0 +1,21 @@
import { getGuildForRequest, hasGuildPermission } from "@/src/services/guildService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { GuildPermission, IGuildRank } from "@/src/types/guildTypes";
import { RequestHandler } from "express";
export const customizeGuildRanksController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const guild = await getGuildForRequest(req);
const payload = JSON.parse(String(req.body)) as ICustomizeGuildRanksRequest;
if (!(await hasGuildPermission(guild, accountId, GuildPermission.Ruler))) {
res.status(400).json("Invalid permission");
return;
}
guild.Ranks = payload.GuildRanks;
await guild.save();
res.end();
};
interface ICustomizeGuildRanksRequest {
GuildRanks: IGuildRank[];
}

View File

@ -0,0 +1,14 @@
import { GuildMember } from "@/src/models/guildModel";
import { getAccountForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const declineGuildInviteController: RequestHandler = async (req, res) => {
const accountId = await getAccountForRequest(req);
await GuildMember.deleteOne({
accountId: accountId,
guildId: req.query.clanId as string
});
res.end();
};

View File

@ -0,0 +1,33 @@
import {
getDojoClient,
getGuildForRequestEx,
hasAccessToDojo,
hasGuildPermission,
removeDojoDeco
} from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
import { RequestHandler } from "express";
export const destroyDojoDecoController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "GuildId LevelKeys");
const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Decorator))) {
res.json({ DojoRequestStatus: -1 });
return;
}
const request = JSON.parse(String(req.body)) as IDestroyDojoDecoRequest;
removeDojoDeco(guild, request.ComponentId, request.DecoId);
await guild.save();
res.json(await getDojoClient(guild, 0, request.ComponentId));
};
interface IDestroyDojoDecoRequest {
DecoType: string;
ComponentId: string;
DecoId: string;
}

View File

@ -0,0 +1,66 @@
import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, scaleRequiredCount } from "@/src/services/guildService";
import { getInventory, updateCurrency } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IDojoContributable } from "@/src/types/guildTypes";
import { RequestHandler } from "express";
import { ExportDojoRecipes, IDojoBuild } from "warframe-public-export-plus";
interface IDojoComponentRushRequest {
DecoType?: string;
DecoId?: string;
ComponentId: string;
Amount: number;
VaultAmount: number;
AllianceVaultAmount: number;
}
export const dojoComponentRushController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
if (!hasAccessToDojo(inventory)) {
res.json({ DojoRequestStatus: -1 });
return;
}
const guild = await getGuildForRequestEx(req, inventory);
const request = JSON.parse(String(req.body)) as IDojoComponentRushRequest;
const component = guild.DojoComponents.id(request.ComponentId)!;
let platinumDonated = request.Amount;
const inventoryChanges = updateCurrency(inventory, request.Amount, true);
if (request.VaultAmount) {
platinumDonated += request.VaultAmount;
guild.VaultPremiumCredits! -= request.VaultAmount;
}
if (request.DecoId) {
const deco = component.Decos!.find(x => x._id.equals(request.DecoId))!;
const meta = Object.values(ExportDojoRecipes.decos).find(x => x.resultType == deco.Type)!;
processContribution(deco, meta, platinumDonated);
} else {
const meta = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == component.pf)!;
processContribution(component, meta, platinumDonated);
const entry = guild.RoomChanges?.find(x => x.componentId.equals(component._id));
if (entry) {
entry.dateTime = component.CompletionTime!;
}
}
await guild.save();
await inventory.save();
res.json({
...(await getDojoClient(guild, 0, component._id)),
InventoryChanges: inventoryChanges
});
};
const processContribution = (component: IDojoContributable, meta: IDojoBuild, platinumDonated: number): void => {
const fullPlatinumCost = scaleRequiredCount(meta.skipTimePrice);
const fullDurationSeconds = meta.time;
const secondsPerPlatinum = fullDurationSeconds / fullPlatinumCost;
component.CompletionTime = new Date(
component.CompletionTime!.getTime() - secondsPerPlatinum * platinumDonated * 1000
);
component.RushPlatinum ??= 0;
component.RushPlatinum += platinumDonated;
};

View File

@ -1,7 +1,143 @@
import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers";
import { config } from "@/src/services/configService";
import { addMiscItems, getInventory } from "@/src/services/inventoryService";
import { fromStoreItem } from "@/src/services/itemDataService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getRandomInt, getRandomWeightedRewardUc } from "@/src/services/rngService";
import { IMongoDate, IOid } from "@/src/types/commonTypes";
import { IDroneClient } from "@/src/types/inventoryTypes/inventoryTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { RequestHandler } from "express";
import { ExportDrones, ExportResources, ExportSystems } from "warframe-public-export-plus";
const dronesController: RequestHandler = (_req, res) => {
res.json({});
export const dronesController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
if ("GetActive" in req.query) {
const inventory = await getInventory(accountId, "Drones");
const activeDrones: IActiveDrone[] = [];
for (const drone of inventory.Drones) {
if (drone.DeployTime) {
activeDrones.push({
DeployTime: toMongoDate(drone.DeployTime),
System: drone.System!,
ItemId: toOid(drone._id),
ItemType: drone.ItemType,
CurrentHP: drone.CurrentHP,
DamageTime: toMongoDate(drone.DamageTime!),
PendingDamage: drone.PendingDamage!,
Resources: [
{
ItemType: drone.ResourceType!,
BinTotal: drone.ResourceCount!,
StartTime: toMongoDate(drone.DeployTime)
}
]
});
}
}
res.json({
ActiveDrones: activeDrones
});
} else if ("droneId" in req.query && "systemIndex" in req.query) {
const inventory = await getInventory(accountId, "Drones");
const drone = inventory.Drones.id(req.query.droneId as string)!;
const droneMeta = ExportDrones[drone.ItemType];
drone.DeployTime = config.instantResourceExtractorDrones ? new Date(0) : new Date();
if (drone.RepairStart) {
const repairMinutes = (Date.now() - drone.RepairStart.getTime()) / 60_000;
const hpPerMinute = droneMeta.repairRate / 60;
drone.CurrentHP = Math.min(drone.CurrentHP + Math.round(repairMinutes * hpPerMinute), droneMeta.durability);
drone.RepairStart = undefined;
}
drone.System = parseInt(req.query.systemIndex as string);
const system = ExportSystems[drone.System - 1];
drone.DamageTime = config.instantResourceExtractorDrones
? new Date()
: new Date(Date.now() + getRandomInt(3 * 3600 * 1000, 4 * 3600 * 1000));
drone.PendingDamage =
Math.random() < system.damageChance
? getRandomInt(system.droneDamage.minValue, system.droneDamage.maxValue)
: 0;
const resource = getRandomWeightedRewardUc(system.resources, droneMeta.probabilities)!;
//logger.debug(`drone rolled`, resource);
drone.ResourceType = fromStoreItem(resource.StoreItem);
const resourceMeta = ExportResources[drone.ResourceType];
if (resourceMeta.pickupQuantity) {
const pickupsToCollect = droneMeta.binCapacity * droneMeta.capacityMultipliers[resource.Rarity];
drone.ResourceCount = 0;
for (let i = 0; i != pickupsToCollect; ++i) {
drone.ResourceCount += getRandomInt(
resourceMeta.pickupQuantity.minValue,
resourceMeta.pickupQuantity.maxValue
);
}
} else {
drone.ResourceCount = 1;
}
await inventory.save();
res.json({});
} else if ("collectDroneId" in req.query) {
const inventory = await getInventory(accountId);
const drone = inventory.Drones.id(req.query.collectDroneId as string)!;
if (new Date() >= drone.DamageTime!) {
drone.CurrentHP -= drone.PendingDamage!;
drone.RepairStart = new Date();
}
const inventoryChanges: IInventoryChanges = {};
if (drone.CurrentHP <= 0) {
inventory.RegularCredits += 100;
inventoryChanges.RegularCredits = 100;
inventory.Drones.pull({ _id: req.query.collectDroneId as string });
inventoryChanges.RemovedIdItems = [
{
ItemId: { $oid: req.query.collectDroneId }
}
];
} else {
const completionTime = drone.DeployTime!.getTime() + ExportDrones[drone.ItemType].fillRate * 3600_000;
if (Date.now() >= completionTime) {
const miscItemChanges = [
{
ItemType: drone.ResourceType!,
ItemCount: drone.ResourceCount!
}
];
addMiscItems(inventory, miscItemChanges);
inventoryChanges.MiscItems = miscItemChanges;
}
drone.DeployTime = undefined;
drone.System = undefined;
drone.DamageTime = undefined;
drone.PendingDamage = undefined;
drone.ResourceType = undefined;
drone.ResourceCount = undefined;
inventoryChanges.Drones = [drone.toJSON<IDroneClient>()];
}
await inventory.save();
res.json({
InventoryChanges: inventoryChanges
});
} else {
throw new Error(`drones.php query not handled`);
}
};
export { dronesController };
interface IActiveDrone {
DeployTime: IMongoDate;
System: number;
ItemId: IOid;
ItemType: string;
CurrentHP: number;
DamageTime: IMongoDate;
PendingDamage: number;
Resources: {
ItemType: string;
BinTotal: number;
StartTime: IMongoDate;
}[];
}

View File

@ -0,0 +1,60 @@
import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory } from "@/src/services/inventoryService";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { TEndlessXpCategory } from "@/src/types/inventoryTypes/inventoryTypes";
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
}
]
}
// ...
]
}
});
};
interface IEndlessXpRequest {
Mode: string; // "r"
Category: TEndlessXpCategory;
Choices: string[];
}

View File

@ -0,0 +1,69 @@
import { toMongoDate } from "@/src/helpers/inventoryHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const entratiLabConquestModeController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(
accountId,
"EntratiVaultCountResetDate EntratiVaultCountLastPeriod EntratiLabConquestUnlocked EchoesHexConquestUnlocked EchoesHexConquestActiveFrameVariants EchoesHexConquestActiveStickers EntratiLabConquestActiveFrameVariants EntratiLabConquestCacheScoreMission EchoesHexConquestCacheScoreMission"
);
const body = getJSONfromString<IEntratiLabConquestModeRequest>(String(req.body));
if (!inventory.EntratiVaultCountResetDate || Date.now() >= inventory.EntratiVaultCountResetDate.getTime()) {
const EPOCH = 1734307200 * 1000; // Mondays, amirite?
const day = Math.trunc((Date.now() - EPOCH) / 86400000);
const week = Math.trunc(day / 7);
const weekStart = EPOCH + week * 604800000;
const weekEnd = weekStart + 604800000;
inventory.EntratiVaultCountLastPeriod = 0;
inventory.EntratiVaultCountResetDate = new Date(weekEnd);
if (inventory.EntratiLabConquestUnlocked) {
inventory.EntratiLabConquestUnlocked = 0;
inventory.EntratiLabConquestActiveFrameVariants = [];
}
if (inventory.EchoesHexConquestUnlocked) {
inventory.EchoesHexConquestUnlocked = 0;
inventory.EchoesHexConquestActiveFrameVariants = [];
inventory.EchoesHexConquestActiveStickers = [];
}
}
if (body.BuyMode) {
inventory.EntratiVaultCountLastPeriod! += 2;
if (body.IsEchoesDeepArchemedea) {
inventory.EchoesHexConquestUnlocked = 1;
} else {
inventory.EntratiLabConquestUnlocked = 1;
}
}
if (body.IsEchoesDeepArchemedea) {
if (inventory.EchoesHexConquestUnlocked) {
inventory.EchoesHexConquestActiveFrameVariants = body.EchoesHexConquestActiveFrameVariants!;
inventory.EchoesHexConquestActiveStickers = body.EchoesHexConquestActiveStickers!;
}
} else {
if (inventory.EntratiLabConquestUnlocked) {
inventory.EntratiLabConquestActiveFrameVariants = body.EntratiLabConquestActiveFrameVariants!;
}
}
await inventory.save();
res.json({
EntratiVaultCountResetDate: toMongoDate(inventory.EntratiVaultCountResetDate),
EntratiVaultCountLastPeriod: inventory.EntratiVaultCountLastPeriod,
EntratiLabConquestUnlocked: inventory.EntratiLabConquestUnlocked,
EntratiLabConquestCacheScoreMission: inventory.EntratiLabConquestCacheScoreMission,
EchoesHexConquestUnlocked: inventory.EchoesHexConquestUnlocked,
EchoesHexConquestCacheScoreMission: inventory.EchoesHexConquestCacheScoreMission
});
};
interface IEntratiLabConquestModeRequest {
BuyMode?: number;
IsEchoesDeepArchemedea?: number;
EntratiLabConquestUnlocked?: number;
EntratiLabConquestActiveFrameVariants?: string[];
EchoesHexConquestUnlocked?: number;
EchoesHexConquestActiveFrameVariants?: string[];
EchoesHexConquestActiveStickers?: string[];
}

View File

@ -1,32 +1,48 @@
import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory } from "@/src/services/inventoryService";
import { addMiscItems, getInventory } from "@/src/services/inventoryService";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { WeaponTypeInternal } from "@/src/services/itemDataService";
import { getRecipe, WeaponTypeInternal } from "@/src/services/itemDataService";
import { EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const evolveWeaponController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const payload = getJSONfromString(String(req.body)) as IEvolveWeaponRequest;
console.assert(payload.Action == "EWA_INSTALL");
const payload = getJSONfromString<IEvolveWeaponRequest>(String(req.body));
// TODO: We should remove the Genesis item & its resources, but currently we don't know these "recipes".
const recipe = getRecipe(payload.Recipe)!;
if (payload.Action == "EWA_INSTALL") {
addMiscItems(
inventory,
recipe.ingredients.map(x => ({ ItemType: x.ItemType, ItemCount: x.ItemCount * -1 }))
);
const item = inventory[payload.Category].find(item => item._id.toString() == (req.query.ItemId as string))!;
item.Features ??= 0;
item.Features |= EquipmentFeatures.INCARNON_GENESIS;
const item = inventory[payload.Category].find(item => item._id.toString() == (req.query.ItemId as string))!;
item.Features ??= 0;
item.Features |= EquipmentFeatures.INCARNON_GENESIS;
item.SkillTree = "0";
item.SkillTree = "0";
inventory.EvolutionProgress ??= [];
if (!inventory.EvolutionProgress.find(entry => entry.ItemType == payload.EvoType)) {
inventory.EvolutionProgress.push({
Progress: 0,
Rank: 1,
ItemType: payload.EvoType
});
inventory.EvolutionProgress ??= [];
if (!inventory.EvolutionProgress.find(entry => entry.ItemType == payload.EvoType)) {
inventory.EvolutionProgress.push({
Progress: 0,
Rank: 1,
ItemType: payload.EvoType
});
}
} else if (payload.Action == "EWA_UNINSTALL") {
addMiscItems(inventory, [
{
ItemType: recipe.resultType,
ItemCount: 1
}
]);
const item = inventory[payload.Category].find(item => item._id.toString() == (req.query.ItemId as string))!;
item.Features! &= ~EquipmentFeatures.INCARNON_GENESIS;
} else {
throw new Error(`unexpected evolve weapon action: ${payload.Action}`);
}
await inventory.save();
@ -34,7 +50,7 @@ export const evolveWeaponController: RequestHandler = async (req, res) => {
};
interface IEvolveWeaponRequest {
Action: "EWA_INSTALL";
Action: string;
Category: WeaponTypeInternal;
Recipe: string; // e.g. "/Lotus/Types/Items/MiscItems/IncarnonAdapters/UnlockerBlueprints/DespairIncarnonBlueprint"
UninstallRecipe: "";

View File

@ -1,31 +1,28 @@
import { RequestHandler } from "express";
import { getSession } from "@/src/managers/sessionManager";
import { logger } from "@/src/utils/logger";
import { IFindSessionRequest } from "@/src/types/session";
//TODO: cleanup
const findSessionsController: RequestHandler = (_req, res) => {
const reqBody = JSON.parse(String(_req.body));
logger.debug("FindSession Request ", { reqBody });
const req = JSON.parse(String(_req.body));
export const findSessionsController: RequestHandler = (_req, res) => {
const req = JSON.parse(String(_req.body)) as IFindSessionRequest;
logger.debug("FindSession Request ", req);
if (req.id != undefined) {
logger.debug("Found ID");
const session = getSession(req.id as string);
const session = getSession(req.id);
if (session) res.json({ queryId: req.queryId, Sessions: session });
if (session.length) res.json({ queryId: req.queryId, Sessions: session });
else res.json({});
} else if (req.originalSessionId != undefined) {
logger.debug("Found OriginalSessionID");
const session = getSession(req.originalSessionId as string);
if (session) res.json({ queryId: req.queryId, Sessions: session });
const session = getSession(req.originalSessionId);
if (session.length) res.json({ queryId: req.queryId, Sessions: session });
else res.json({});
} else {
logger.debug("Found SessionRequest");
const session = getSession(String(_req.body));
if (session) res.json({ queryId: req.queryId, Sessions: session });
const session = getSession(req);
if (session.length) res.json({ queryId: req.queryId, Sessions: session });
else res.json({});
}
};
export { findSessionsController };

View File

@ -0,0 +1,65 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper";
import { addMiscItems, getInventory, getStandingLimit, updateStandingLimit } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express";
import { ExportResources, ExportSyndicates } from "warframe-public-export-plus";
export const fishmongerController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const body = getJSONfromString<IFishmongerRequest>(String(req.body));
const miscItemChanges: IMiscItem[] = [];
let syndicateTag: string | undefined;
let gainedStanding = 0;
for (const fish of body.Fish) {
const fishData = ExportResources[fish.ItemType];
if (req.query.dissect == "1") {
for (const part of fishData.dissectionParts!) {
const partItem = miscItemChanges.find(x => x.ItemType == part.ItemType);
if (partItem) {
partItem.ItemCount += part.ItemCount * fish.ItemCount;
} else {
miscItemChanges.push({ ItemType: part.ItemType, ItemCount: part.ItemCount * fish.ItemCount });
}
}
} else {
syndicateTag = fishData.syndicateTag!;
gainedStanding += fishData.standingBonus! * fish.ItemCount;
}
miscItemChanges.push({ ItemType: fish.ItemType, ItemCount: fish.ItemCount * -1 });
}
addMiscItems(inventory, miscItemChanges);
if (gainedStanding && syndicateTag) {
let syndicate = inventory.Affiliations.find(x => x.Tag == syndicateTag);
if (!syndicate) {
syndicate = inventory.Affiliations[inventory.Affiliations.push({ Tag: syndicateTag, Standing: 0 }) - 1];
}
const syndicateMeta = ExportSyndicates[syndicateTag];
const max = getMaxStanding(syndicateMeta, syndicate.Title ?? 0);
if (syndicate.Standing + gainedStanding > max) {
gainedStanding = max - syndicate.Standing;
}
if (gainedStanding > getStandingLimit(inventory, syndicateMeta.dailyLimitBin)) {
gainedStanding = getStandingLimit(inventory, syndicateMeta.dailyLimitBin);
}
syndicate.Standing += gainedStanding;
updateStandingLimit(inventory, syndicateMeta.dailyLimitBin, gainedStanding);
}
await inventory.save();
res.json({
InventoryChanges: {
MiscItems: miscItemChanges
},
SyndicateTag: syndicateTag,
StandingChange: gainedStanding
});
};
interface IFishmongerRequest {
Fish: IMiscItem[];
}

View File

@ -1,19 +1,42 @@
import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory, addMiscItems, addEquipment } from "@/src/services/inventoryService";
import { IMiscItem, TFocusPolarity } from "@/src/types/inventoryTypes/inventoryTypes";
import { getInventory, addMiscItems, addEquipment, occupySlot } from "@/src/services/inventoryService";
import { IMiscItem, TFocusPolarity, TEquipmentKey, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger";
import { ExportFocusUpgrades } from "warframe-public-export-plus";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const focusController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
switch (req.query.op) {
default:
logger.error("Unhandled focus op type: " + req.query.op);
logger.debug(req.body.toString());
logger.error("Unhandled focus op type: " + String(req.query.op));
logger.debug(String(req.body));
res.end();
break;
case FocusOperation.InstallLens: {
const request = JSON.parse(String(req.body)) as ILensInstallRequest;
const inventory = await getInventory(accountId);
for (const item of inventory[request.Category]) {
if (item._id.toString() == request.WeaponId) {
item.FocusLens = request.LensType;
addMiscItems(inventory, [
{
ItemType: request.LensType,
ItemCount: -1
} satisfies IMiscItem
]);
break;
}
}
await inventory.save();
res.json({
weaponId: request.WeaponId,
lensType: request.LensType
});
break;
}
case FocusOperation.UnlockWay: {
const focusType = (JSON.parse(String(req.body)) as IWayRequest).FocusType;
const focusPolarity = focusTypeToPolarity(focusType);
@ -33,9 +56,16 @@ export const focusController: RequestHandler = async (req, res) => {
}
case FocusOperation.ActivateWay: {
const focusType = (JSON.parse(String(req.body)) as IWayRequest).FocusType;
const inventory = await getInventory(accountId);
inventory.FocusAbility = focusType;
await inventory.save();
await Inventory.updateOne(
{
accountOwnerId: accountId
},
{
FocusAbility: focusType
}
);
res.end();
break;
}
@ -48,7 +78,7 @@ export const focusController: RequestHandler = async (req, res) => {
cost += ExportFocusUpgrades[focusType].baseFocusPointCost;
inventory.FocusUpgrades.push({ ItemType: focusType, Level: 0 });
}
inventory.FocusXP[focusPolarity] -= cost;
inventory.FocusXP![focusPolarity] -= cost;
await inventory.save();
res.json({
FocusTypes: request.FocusTypes,
@ -66,7 +96,7 @@ export const focusController: RequestHandler = async (req, res) => {
const focusUpgradeDb = inventory.FocusUpgrades.find(entry => entry.ItemType == focusUpgrade.ItemType)!;
focusUpgradeDb.Level = focusUpgrade.Level;
}
inventory.FocusXP[focusPolarity] -= cost;
inventory.FocusXP![focusPolarity] -= cost;
await inventory.save();
res.json({
FocusInfos: request.FocusInfos,
@ -81,15 +111,18 @@ export const focusController: RequestHandler = async (req, res) => {
"/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingChassis",
"/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingBarrel"
];
const result = await addEquipment("OperatorAmps", request.StartingWeaponType, accountId, parts);
res.json(result);
const inventory = await getInventory(accountId);
const inventoryChanges = addEquipment(inventory, "OperatorAmps", request.StartingWeaponType, parts);
occupySlot(inventory, InventorySlot.AMPS, false);
await inventory.save();
res.json((inventoryChanges.OperatorAmps as IEquipmentClient[])[0]);
break;
}
case FocusOperation.UnbindUpgrade: {
const request = JSON.parse(String(req.body)) as IUnbindUpgradeRequest;
const focusPolarity = focusTypeToPolarity(request.FocusTypes[0]);
const inventory = await getInventory(accountId);
inventory.FocusXP[focusPolarity] -= 750_000 * request.FocusTypes.length;
inventory.FocusXP![focusPolarity] -= 750_000 * request.FocusTypes.length;
addMiscItems(inventory, [
{
ItemType: "/Lotus/Types/Gameplay/Eidolon/Resources/SentientShards/SentientShardBrilliantItem",
@ -144,6 +177,7 @@ export const focusController: RequestHandler = async (req, res) => {
};
enum FocusOperation {
InstallLens = "1",
UnlockWay = "2",
UnlockUpgrade = "3",
LevelUpUpgrade = "4",
@ -186,6 +220,12 @@ interface ISentTrainingAmplifierRequest {
StartingWeaponType: string;
}
interface ILensInstallRequest {
LensType: string;
Category: TEquipmentKey;
WeaponId: string;
}
// Works for ways & upgrades
const focusTypeToPolarity = (type: string): TFocusPolarity => {
return ("AP_" + type.substr(1).split("/")[3].toUpperCase()) as TFocusPolarity;

View File

@ -0,0 +1,52 @@
import { RequestHandler } from "express";
import { ExportResources } from "warframe-public-export-plus";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { addFusionTreasures, addMiscItems, getInventory } from "@/src/services/inventoryService";
import { IFusionTreasure, IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
interface IFusionTreasureRequest {
oldTreasureName: string;
newTreasureName: string;
}
const parseFusionTreasure = (name: string, count: number): IFusionTreasure => {
const arr = name.split("_");
return {
ItemType: arr[0],
Sockets: parseInt(arr[1], 16),
ItemCount: count
};
};
export const fusionTreasuresController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const request = JSON.parse(String(req.body)) as IFusionTreasureRequest;
// Swap treasures
const oldTreasure = parseFusionTreasure(request.oldTreasureName, -1);
const newTreasure = parseFusionTreasure(request.newTreasureName, 1);
const fusionTreasureChanges = [oldTreasure, newTreasure];
addFusionTreasures(inventory, fusionTreasureChanges);
// Remove consumed stars
const miscItemChanges: IMiscItem[] = [];
const filledSockets = newTreasure.Sockets & ~oldTreasure.Sockets;
for (let i = 0; filledSockets >> i; ++i) {
if ((filledSockets >> i) & 1) {
//console.log("Socket", i, "has been filled with", ExportResources[oldTreasure.ItemType].sockets![i]);
miscItemChanges.push({
ItemType: ExportResources[oldTreasure.ItemType].sockets![i],
ItemCount: -1
});
}
}
addMiscItems(inventory, miscItemChanges);
await inventory.save();
// The response itself is the inventory changes for this endpoint.
res.json({
MiscItems: miscItemChanges,
FusionTreasures: fusionTreasureChanges
});
};

View File

@ -7,10 +7,9 @@ import { IGenericUpdate } from "@/src/types/genericUpdate";
// This endpoint used to be /api/genericUpdate.php, but sometime around the Jade Shadows update, it was changed to /api/updateNodeIntros.php.
// SpaceNinjaServer supports both endpoints right now.
// eslint-disable-next-line @typescript-eslint/no-misused-promises
const genericUpdateController: RequestHandler = async (request, response) => {
const accountId = await getAccountIdForRequest(request);
const update = getJSONfromString(String(request.body)) as IGenericUpdate;
const update = getJSONfromString<IGenericUpdate>(String(request.body));
response.json(await updateGeneric(update, accountId));
};

View File

@ -1,33 +0,0 @@
import { RequestHandler } from "express";
import { config } from "@/src/services/configService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory } from "@/src/services/inventoryService";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const getCreditsController: RequestHandler = async (req, res) => {
let accountId;
try {
accountId = await getAccountIdForRequest(req);
} catch (e) {
res.status(400).send("Log-in expired");
return;
}
if (config.infiniteResources) {
res.json({
RegularCredits: 999999999,
TradesRemaining: 999999999,
PremiumCreditsFree: 999999999,
PremiumCredits: 999999999
});
return;
}
const inventory = await getInventory(accountId);
res.json({
RegularCredits: inventory.RegularCredits,
TradesRemaining: inventory.TradesRemaining,
PremiumCreditsFree: inventory.PremiumCreditsFree,
PremiumCredits: inventory.PremiumCredits
});
};

View File

@ -1,6 +1,7 @@
import { Request, Response } from "express";
const getFriendsController = (_request: Request, response: Response) => {
// 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",

View File

@ -1,74 +1,34 @@
import { RequestHandler } from "express";
import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
import { Guild } from "@/src/models/guildModel";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { toOid } from "@/src/helpers/inventoryHelpers";
import { logger } from "@/src/utils/logger";
import { getInventory } from "@/src/services/inventoryService";
import { createUniqueClanName, getGuildClient } from "@/src/services/guildService";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
const getGuildController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await Inventory.findOne({ accountOwnerId: accountId });
if (!inventory) {
res.status(400).json({ error: "inventory was undefined" });
return;
}
const inventory = await getInventory(accountId, "GuildId");
if (inventory.GuildId) {
const guild = await Guild.findOne({ _id: inventory.GuildId });
if (guild) {
res.json({
_id: toOid(guild._id),
Name: guild.Name,
Members: [
{
_id: { $oid: req.query.accountId },
Rank: 0,
Status: 0
}
],
Ranks: [
{
Name: "/Lotus/Language/Game/Rank_Creator",
Permissions: 16351
},
{
Name: "/Lotus/Language/Game/Rank_Warlord",
Permissions: 14303
},
{
Name: "/Lotus/Language/Game/Rank_General",
Permissions: 4318
},
{
Name: "/Lotus/Language/Game/Rank_Officer",
Permissions: 4314
},
{
Name: "/Lotus/Language/Game/Rank_Leader",
Permissions: 4106
},
{
Name: "/Lotus/Language/Game/Rank_Sage",
Permissions: 4304
},
{
Name: "/Lotus/Language/Game/Rank_Soldier",
Permissions: 4098
},
{
Name: "/Lotus/Language/Game/Rank_Initiate",
Permissions: 4096
},
{
Name: "/Lotus/Language/Game/Rank_Utility",
Permissions: 4096
}
],
Tier: 1
});
// Handle guilds created before we added discriminators
if (guild.Name.indexOf("#") == -1) {
guild.Name = await createUniqueClanName(guild.Name);
await guild.save();
}
if (guild.CeremonyResetDate && Date.now() >= guild.CeremonyResetDate.getTime()) {
logger.debug(`ascension ceremony is over`);
guild.CeremonyEndo = undefined;
guild.CeremonyContributors = undefined;
guild.CeremonyResetDate = undefined;
await guild.save();
}
res.json(await getGuildClient(guild, accountId));
return;
}
}
res.json({});
res.sendStatus(200);
};
export { getGuildController };

View File

@ -1,10 +1,8 @@
import { RequestHandler } from "express";
import { Types } from "mongoose";
import { Guild } from "@/src/models/guildModel";
import { IDojoClient, IDojoComponentClient } from "@/src/types/guildTypes";
import { toOid, toMongoDate } from "@/src/helpers/inventoryHelpers";
import { getDojoClient } from "@/src/services/guildService";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const getGuildDojoController: RequestHandler = async (req, res) => {
const guildId = req.query.guildId as string;
@ -15,46 +13,21 @@ export const getGuildDojoController: RequestHandler = async (req, res) => {
}
// Populate dojo info if not present
if (!guild.DojoComponents || guild.DojoComponents.length == 0) {
guild.DojoComponents = [
{
_id: new Types.ObjectId(),
pf: "/Lotus/Levels/ClanDojo/DojoHall.level",
ppf: "",
CompletionTime: new Date(Date.now())
}
];
if (guild.DojoComponents.length == 0) {
guild.DojoComponents.push({
_id: new Types.ObjectId(),
pf: "/Lotus/Levels/ClanDojo/DojoHall.level",
ppf: "",
CompletionTime: new Date(Date.now()),
DecoCapacity: 600
});
await guild.save();
}
const dojo: IDojoClient = {
_id: { $oid: guildId },
Name: guild.Name,
Tier: 1,
FixedContributions: true,
DojoRevision: 1,
RevisionTime: Math.round(Date.now() / 1000),
Energy: 5,
Capacity: 100,
DojoRequestStatus: 0,
DojoComponents: []
};
guild.DojoComponents.forEach(dojoComponent => {
const clientComponent: IDojoComponentClient = {
id: toOid(dojoComponent._id),
pf: dojoComponent.pf,
ppf: dojoComponent.ppf,
DecoCapacity: 600
};
if (dojoComponent.pi) {
clientComponent.pi = toOid(dojoComponent.pi);
clientComponent.op = dojoComponent.op!;
clientComponent.pp = dojoComponent.pp!;
}
if (dojoComponent.CompletionTime) {
clientComponent.CompletionTime = toMongoDate(dojoComponent.CompletionTime);
}
dojo.DojoComponents.push(clientComponent);
});
res.json(dojo);
const payload: IGetGuildDojoRequest = req.body ? (JSON.parse(String(req.body)) as IGetGuildDojoRequest) : {};
res.json(await getDojoClient(guild, 0, payload.ComponentId));
};
interface IGetGuildDojoRequest {
ComponentId?: string;
}

View File

@ -1,11 +1,60 @@
import { toMongoDate } from "@/src/helpers/inventoryHelpers";
import { Guild } from "@/src/models/guildModel";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IMongoDate } from "@/src/types/commonTypes";
import { RequestHandler } from "express";
export const getGuildLogController: RequestHandler = (_req, res) => {
res.json({
RoomChanges: [],
TechChanges: [],
RosterActivity: [],
StandingsUpdates: [],
ClassChanges: []
});
export const getGuildLogController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "GuildId");
if (inventory.GuildId) {
const guild = await Guild.findOne({ _id: inventory.GuildId });
if (guild) {
const log: Record<string, IGuildLogEntryClient[]> = {
RoomChanges: [],
TechChanges: [],
RosterActivity: [],
StandingsUpdates: [],
ClassChanges: []
};
guild.RoomChanges?.forEach(entry => {
log.RoomChanges.push({
dateTime: toMongoDate(entry.dateTime ?? new Date()),
entryType: entry.entryType,
details: entry.details
});
});
guild.TechChanges?.forEach(entry => {
log.TechChanges.push({
dateTime: toMongoDate(entry.dateTime ?? new Date()),
entryType: entry.entryType,
details: entry.details
});
});
guild.RosterActivity?.forEach(entry => {
log.RosterActivity.push({
dateTime: toMongoDate(entry.dateTime),
entryType: entry.entryType,
details: entry.details
});
});
guild.ClassChanges?.forEach(entry => {
log.ClassChanges.push({
dateTime: toMongoDate(entry.dateTime),
entryType: entry.entryType,
details: entry.details
});
});
res.json(log);
return;
}
}
res.sendStatus(200);
};
interface IGuildLogEntryClient {
dateTime: IMongoDate;
entryType: number;
details: number | string;
}

View File

@ -1,13 +1,26 @@
import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express";
const getNewRewardSeedController: RequestHandler = (_req, res) => {
res.json({ rewardSeed: generateRewardSeed() });
export const getNewRewardSeedController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const rewardSeed = generateRewardSeed();
logger.debug(`generated new reward seed: ${rewardSeed}`);
await Inventory.updateOne(
{
accountOwnerId: accountId
},
{
RewardSeed: rewardSeed
}
);
res.json({ rewardSeed: rewardSeed });
};
function generateRewardSeed(): number {
export function generateRewardSeed(): number {
const min = -Number.MAX_SAFE_INTEGER;
const max = Number.MAX_SAFE_INTEGER;
return Math.floor(Math.random() * (max - min + 1)) + min;
}
export { getNewRewardSeedController };

View File

@ -4,31 +4,32 @@ import allShipFeatures from "@/static/fixed_responses/allShipFeatures.json";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getPersonalRooms } from "@/src/services/personalRoomsService";
import { getShip } from "@/src/services/shipService";
import { Loadout } from "@/src/models/inventoryModels/loadoutModel";
import { logger } from "@/src/utils/logger";
import { toOid } from "@/src/helpers/inventoryHelpers";
import { IGetShipResponse } from "@/src/types/shipTypes";
import { IPersonalRooms } from "@/src/types/personalRoomsTypes";
import { getLoadout } from "@/src/services/loadoutService";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const getShipController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const personalRooms = await getPersonalRooms(accountId);
const personalRoomsDb = await getPersonalRooms(accountId);
const personalRooms = personalRoomsDb.toJSON<IPersonalRooms>();
const loadout = await getLoadout(accountId);
const ship = await getShip(personalRooms.activeShipId, "ShipInteriorColors ShipAttachments SkinFlavourItem");
const ship = await getShip(personalRoomsDb.activeShipId, "ShipAttachments SkinFlavourItem");
const getShipResponse: IGetShipResponse = {
ShipOwnerId: accountId,
LoadOutInventory: { LoadOutPresets: loadout.toJSON() },
Ship: {
...personalRooms.toJSON().Ship,
ShipId: toOid(personalRooms.activeShipId),
...personalRooms.Ship,
ShipId: toOid(personalRoomsDb.activeShipId),
ShipInterior: {
Colors: ship.ShipInteriorColors,
Colors: personalRooms.ShipInteriorColors,
ShipAttachments: ship.ShipAttachments,
SkinFlavourItem: ship.SkinFlavourItem
}
},
Apartment: personalRooms.Apartment
Apartment: personalRooms.Apartment,
TailorShop: personalRooms.TailorShop
};
if (config.unlockAllShipFeatures) {
@ -37,14 +38,3 @@ export const getShipController: RequestHandler = async (req, res) => {
res.json(getShipResponse);
};
export const getLoadout = async (accountId: string) => {
const loadout = await Loadout.findOne({ loadoutOwnerId: accountId });
if (!loadout) {
logger.error(`loadout not found for account ${accountId}`);
throw new Error("loadout not found");
}
return loadout;
};

View File

@ -1,23 +1,14 @@
import { RequestHandler } from "express";
import ArchimedeanVendorManifest from "@/static/fixed_responses/getVendorInfo/ArchimedeanVendorManifest.json";
import MaskSalesmanManifest from "@/static/fixed_responses/getVendorInfo/MaskSalesmanManifest.json";
import ZarimanCommisionsManifestArchimedean from "@/static/fixed_responses/getVendorInfo/ZarimanCommisionsManifestArchimedean.json";
import { getVendorManifestByTypeName } from "@/src/services/serversideVendorsService";
export const getVendorInfoController: RequestHandler = (req, res) => {
switch (req.query.vendor as string) {
case "/Lotus/Types/Game/VendorManifests/Zariman/ArchimedeanVendorManifest":
res.json(ArchimedeanVendorManifest);
break;
case "/Lotus/Types/Game/VendorManifests/Ostron/MaskSalesmanManifest":
res.json(MaskSalesmanManifest);
break;
case "/Lotus/Types/Game/VendorManifests/Zariman/ZarimanCommisionsManifestArchimedean":
res.json(ZarimanCommisionsManifestArchimedean);
break;
default:
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();
}
};

View File

@ -0,0 +1,38 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { crackRelic } from "@/src/helpers/relicHelper";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IVoidTearParticipantInfo } from "@/src/types/requestTypes";
import { RequestHandler } from "express";
export const getVoidProjectionRewardsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const data = getJSONfromString<IVoidProjectionRewardRequest>(String(req.body));
if (data.ParticipantInfo.QualifiesForReward && !data.ParticipantInfo.HaveRewardResponse) {
const inventory = await getInventory(accountId);
await crackRelic(inventory, data.ParticipantInfo);
await inventory.save();
}
const response: IVoidProjectionRewardResponse = {
CurrentWave: data.CurrentWave,
ParticipantInfo: data.ParticipantInfo,
DifficultyTier: data.DifficultyTier
};
res.json(response);
};
interface IVoidProjectionRewardRequest {
CurrentWave: number;
ParticipantInfo: IVoidTearParticipantInfo;
VoidTier: string;
DifficultyTier: number;
VoidProjectionRemovalHash: string;
}
interface IVoidProjectionRewardResponse {
CurrentWave: number;
ParticipantInfo: IVoidTearParticipantInfo;
DifficultyTier: number;
}

View File

@ -1,50 +1,47 @@
import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory } from "@/src/services/inventoryService";
import { addMiscItems, getInventory } from "@/src/services/inventoryService";
import { WeaponTypeInternal } from "@/src/services/itemDataService";
import { ArtifactPolarity, EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { ArtifactPolarity, EquipmentFeatures, IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { ExportRecipes } from "warframe-public-export-plus";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
const modularWeaponCategory: (WeaponTypeInternal | "Hoverboards")[] = [
"LongGuns",
"Pistols",
"Melee",
"OperatorAmps",
"Hoverboards" // Not sure about hoverboards just coppied from modual crafting
"Hoverboards"
];
interface IGildWeaponRequest {
ItemName: string;
Recipe: string; // /Lotus/Weapons/SolarisUnited/LotusGildKitgunBlueprint
Recipe: string; // e.g. /Lotus/Weapons/SolarisUnited/LotusGildKitgunBlueprint
PolarizeSlot?: number;
PolarizeValue?: ArtifactPolarity;
ItemId: string;
Category: WeaponTypeInternal | "Hoverboards";
}
// In export there no recipes for gild action, so reputation and ressources only consumed visually
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const gildWeaponController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const data: IGildWeaponRequest = getJSONfromString(String(req.body));
const data = getJSONfromString<IGildWeaponRequest>(String(req.body));
data.ItemId = String(req.query.ItemId);
if (!modularWeaponCategory.includes(req.query.Category as WeaponTypeInternal | "Hoverboards")) {
throw new Error(`Unknown modular weapon Category: ${req.query.Category}`);
throw new Error(`Unknown modular weapon Category: ${String(req.query.Category)}`);
}
data.Category = req.query.Category as WeaponTypeInternal | "Hoverboards";
const inventory = await getInventory(accountId);
if (!inventory[data.Category]) {
throw new Error(`Category ${req.query.Category} not found in inventory`);
}
const weaponIndex = inventory[data.Category].findIndex(x => String(x._id) === data.ItemId);
if (weaponIndex === -1) {
throw new Error(`Weapon with ${data.ItemId} not found in category ${req.query.Category}`);
throw new Error(`Weapon with ${data.ItemId} not found in category ${String(req.query.Category)}`);
}
const weapon = inventory[data.Category][weaponIndex];
weapon.Features = EquipmentFeatures.GILDED; // maybe 9 idk if DOUBLE_CAPACITY is also given
weapon.Features ??= 0;
weapon.Features |= EquipmentFeatures.GILDED;
weapon.ItemName = data.ItemName;
weapon.XP = 0;
if (data.Category != "OperatorAmps" && data.PolarizeSlot && data.PolarizeValue) {
@ -56,11 +53,29 @@ export const gildWeaponController: RequestHandler = async (req, res) => {
];
}
inventory[data.Category][weaponIndex] = weapon;
await inventory.save();
const inventoryChanges: IInventoryChanges = {};
inventoryChanges[data.Category] = [weapon.toJSON<IEquipmentClient>()];
const recipe = ExportRecipes[data.Recipe];
inventoryChanges.MiscItems = recipe.secretIngredients!.map(ingredient => ({
ItemType: ingredient.ItemType,
ItemCount: ingredient.ItemCount * -1
}));
addMiscItems(inventory, inventoryChanges.MiscItems);
const affiliationMods = [];
if (recipe.syndicateStandingChange) {
const affiliation = inventory.Affiliations.find(x => x.Tag == recipe.syndicateStandingChange!.tag)!;
affiliation.Standing += recipe.syndicateStandingChange.value;
affiliationMods.push({
Tag: recipe.syndicateStandingChange.tag,
Standing: recipe.syndicateStandingChange.value
});
}
await inventory.save();
res.json({
InventoryChanges: {
[data.Category]: [weapon]
}
InventoryChanges: inventoryChanges,
AffiliationMods: affiliationMods
});
};

View File

@ -0,0 +1,23 @@
import { RequestHandler } from "express";
import { parseString } from "@/src/helpers/general";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory } from "@/src/services/inventoryService";
import { IGroup } from "@/src/types/loginTypes";
import { giveKeyChainItem } from "@/src/services/questService";
export const giveKeyChainTriggeredItemsController: RequestHandler = async (req, res) => {
const accountId = parseString(req.query.accountId);
const keyChainInfo = getJSONfromString<IKeyChainRequest>((req.body as string).toString());
const inventory = await getInventory(accountId);
const inventoryChanges = await giveKeyChainItem(inventory, keyChainInfo);
await inventory.save();
res.send(inventoryChanges);
};
export interface IKeyChainRequest {
KeyChain: string;
ChainStage: number;
Groups?: IGroup[];
}

View File

@ -0,0 +1,16 @@
import { IKeyChainRequest } from "@/src/controllers/api/giveKeyChainTriggeredItemsController";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { giveKeyChainMessage } from "@/src/services/questService";
import { RequestHandler } from "express";
export const giveKeyChainTriggeredMessageController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const keyChainInfo = JSON.parse((req.body as Buffer).toString()) as IKeyChainRequest;
const inventory = await getInventory(accountId, "QuestKeys");
await giveKeyChainMessage(inventory, accountId, keyChainInfo);
await inventory.save();
res.send(1);
};

View File

@ -0,0 +1,45 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { addItem, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IOid } from "@/src/types/commonTypes";
import { RequestHandler } from "express";
export const giveQuestKeyRewardController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const rewardRequest = getJSONfromString<IQuestKeyRewardRequest>((req.body as Buffer).toString());
if (Array.isArray(rewardRequest.reward)) {
throw new Error("Multiple rewards not expected");
}
const reward = rewardRequest.reward;
const inventory = await getInventory(accountId);
const inventoryChanges = await addItem(inventory, reward.ItemType, reward.Amount);
await inventory.save();
res.json(inventoryChanges.InventoryChanges);
//TODO: consider whishlist changes
};
export interface IQuestKeyRewardRequest {
reward: IQuestKeyReward;
}
export interface IQuestKeyReward {
RewardType: string;
CouponType: string;
Icon: string;
ItemType: string;
StoreItemType: string;
ProductCategory: string;
Amount: number;
ScalingMultiplier: number;
Durability: string;
DisplayName: string;
Duration: number;
CouponSku: number;
Syndicate: string;
//Milestones: any[];
ChooseSetIndex: number;
NewSystemReward: boolean;
_id: IOid;
}

View File

@ -0,0 +1,97 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { InventoryDocumentProps } from "@/src/models/inventoryModels/inventoryModel";
import {
addEquipment,
addItem,
addPowerSuit,
combineInventoryChanges,
getInventory,
updateSlots
} from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IInventoryClient, IInventoryDatabase, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { RequestHandler } from "express";
import { HydratedDocument } from "mongoose";
type TPartialStartingGear = Pick<IInventoryClient, "LongGuns" | "Suits" | "Pistols" | "Melee">;
export const giveStartingGearController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const startingGear = getJSONfromString<TPartialStartingGear>(String(req.body));
const inventory = await getInventory(accountId);
const inventoryChanges = await addStartingGear(inventory, startingGear);
await inventory.save();
res.send(inventoryChanges);
};
//TODO: RawUpgrades might need to return a LastAdded
const awakeningRewards = [
"/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem1",
"/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem2",
"/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem3",
"/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem4",
"/Lotus/Types/Restoratives/LisetAutoHack",
"/Lotus/Upgrades/Mods/Warframe/AvatarShieldMaxMod"
];
export const addStartingGear = async (
inventory: HydratedDocument<IInventoryDatabase, InventoryDocumentProps>,
startingGear: TPartialStartingGear | undefined = undefined
): Promise<IInventoryChanges> => {
const { LongGuns, Pistols, Suits, Melee } = startingGear || {
LongGuns: [{ ItemType: "/Lotus/Weapons/Tenno/Rifle/Rifle" }],
Pistols: [{ ItemType: "/Lotus/Weapons/Tenno/Pistol/Pistol" }],
Suits: [{ ItemType: "/Lotus/Powersuits/Excalibur/Excalibur" }],
Melee: [{ ItemType: "/Lotus/Weapons/Tenno/Melee/LongSword/LongSword" }]
};
//TODO: properly merge weapon bin changes it is currently static here
const inventoryChanges: IInventoryChanges = {};
addEquipment(inventory, "LongGuns", LongGuns[0].ItemType, undefined, inventoryChanges);
addEquipment(inventory, "Pistols", Pistols[0].ItemType, undefined, inventoryChanges);
addEquipment(inventory, "Melee", Melee[0].ItemType, undefined, inventoryChanges);
addPowerSuit(inventory, Suits[0].ItemType, inventoryChanges);
addEquipment(
inventory,
"DataKnives",
"/Lotus/Weapons/Tenno/HackingDevices/TnHackingDevice/TnHackingDeviceWeapon",
undefined,
inventoryChanges,
{ XP: 450_000 }
);
addEquipment(
inventory,
"Scoops",
"/Lotus/Weapons/Tenno/Speedball/SpeedballWeaponTest",
undefined,
inventoryChanges
);
updateSlots(inventory, InventorySlot.SUITS, 0, 1);
updateSlots(inventory, InventorySlot.WEAPONS, 0, 3);
inventoryChanges.SuitBin = { count: 1, platinum: 0, Slots: -1 };
inventoryChanges.WeaponBin = { count: 3, platinum: 0, Slots: -3 };
await addItem(inventory, "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain");
inventory.ActiveQuest = "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain";
inventory.PremiumCredits = 50;
inventory.PremiumCreditsFree = 50;
inventoryChanges.PremiumCredits = 50;
inventoryChanges.PremiumCreditsFree = 50;
inventory.RegularCredits = 3000;
inventoryChanges.RegularCredits = 3000;
for (const item of awakeningRewards) {
const inventoryDelta = await addItem(inventory, item);
combineInventoryChanges(inventoryChanges, inventoryDelta);
}
inventory.PlayedParkourTutorial = true;
inventory.ReceivedStartingGear = true;
return inventoryChanges;
};

View File

@ -1,5 +1,290 @@
import { RequestHandler } from "express";
import {
getGuildForRequestEx,
getGuildVault,
hasAccessToDojo,
hasGuildPermission,
removePigmentsFromGuildMembers,
scaleRequiredCount
} from "@/src/services/guildService";
import { ExportDojoRecipes, IDojoResearch } from "warframe-public-export-plus";
import { getAccountIdForRequest } from "@/src/services/loginService";
import {
addItem,
addMiscItems,
addRecipes,
combineInventoryChanges,
getInventory,
updateCurrency
} from "@/src/services/inventoryService";
import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { config } from "@/src/services/configService";
import { GuildPermission, ITechProjectClient, ITechProjectDatabase } from "@/src/types/guildTypes";
import { TGuildDatabaseDocument } from "@/src/models/guildModel";
import { toMongoDate } from "@/src/helpers/inventoryHelpers";
import { logger } from "@/src/utils/logger";
export const guildTechController: RequestHandler = (_req, res) => {
res.status(500).end(); // This is what I got for a fresh clan.
export const guildTechController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const guild = await getGuildForRequestEx(req, inventory);
const data = JSON.parse(String(req.body)) as TGuildTechRequest;
if (data.Action == "Sync") {
let needSave = false;
const techProjects: ITechProjectClient[] = [];
if (guild.TechProjects) {
for (const project of guild.TechProjects) {
const techProject: ITechProjectClient = {
ItemType: project.ItemType,
ReqCredits: project.ReqCredits,
ReqItems: project.ReqItems,
State: project.State
};
if (project.CompletionDate) {
techProject.CompletionDate = toMongoDate(project.CompletionDate);
if (Date.now() >= project.CompletionDate.getTime()) {
needSave ||= setTechLogState(guild, project.ItemType, 4, project.CompletionDate);
}
}
techProjects.push(techProject);
}
}
if (needSave) {
await guild.save();
}
res.json({ TechProjects: techProjects });
} else if (data.Action == "Start") {
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
res.status(400).send("-1").end();
return;
}
const recipe = ExportDojoRecipes.research[data.RecipeType];
guild.TechProjects ??= [];
if (!guild.TechProjects.find(x => x.ItemType == data.RecipeType)) {
const techProject =
guild.TechProjects[
guild.TechProjects.push({
ItemType: data.RecipeType,
ReqCredits: config.noDojoResearchCosts ? 0 : scaleRequiredCount(recipe.price),
ReqItems: recipe.ingredients.map(x => ({
ItemType: x.ItemType,
ItemCount: config.noDojoResearchCosts ? 0 : scaleRequiredCount(x.ItemCount)
})),
State: 0
}) - 1
];
setTechLogState(guild, techProject.ItemType, 5);
if (config.noDojoResearchCosts) {
processFundedProject(guild, techProject, recipe);
} else {
if (data.RecipeType.substring(0, 39) == "/Lotus/Types/Items/Research/DojoColors/") {
guild.ActiveDojoColorResearch = data.RecipeType;
}
}
}
await guild.save();
res.end();
} else if (data.Action == "Contribute") {
if (!hasAccessToDojo(inventory)) {
res.status(400).send("-1").end();
return;
}
const contributions = data;
const techProject = guild.TechProjects!.find(x => x.ItemType == contributions.RecipeType)!;
if (contributions.VaultCredits) {
if (contributions.VaultCredits > techProject.ReqCredits) {
contributions.VaultCredits = techProject.ReqCredits;
}
techProject.ReqCredits -= contributions.VaultCredits;
guild.VaultRegularCredits! -= contributions.VaultCredits;
}
if (contributions.RegularCredits > techProject.ReqCredits) {
contributions.RegularCredits = techProject.ReqCredits;
}
techProject.ReqCredits -= contributions.RegularCredits;
if (contributions.VaultMiscItems.length) {
for (const miscItem of contributions.VaultMiscItems) {
const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType);
if (reqItem) {
if (miscItem.ItemCount > reqItem.ItemCount) {
miscItem.ItemCount = reqItem.ItemCount;
}
reqItem.ItemCount -= miscItem.ItemCount;
const vaultMiscItem = guild.VaultMiscItems!.find(x => x.ItemType == miscItem.ItemType)!;
vaultMiscItem.ItemCount -= miscItem.ItemCount;
}
}
}
const miscItemChanges = [];
for (const miscItem of contributions.MiscItems) {
const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType);
if (reqItem) {
if (miscItem.ItemCount > reqItem.ItemCount) {
miscItem.ItemCount = reqItem.ItemCount;
}
reqItem.ItemCount -= miscItem.ItemCount;
miscItemChanges.push({
ItemType: miscItem.ItemType,
ItemCount: miscItem.ItemCount * -1
});
}
}
addMiscItems(inventory, miscItemChanges);
const inventoryChanges: IInventoryChanges = updateCurrency(inventory, contributions.RegularCredits, false);
inventoryChanges.MiscItems = miscItemChanges;
if (techProject.ReqCredits == 0 && !techProject.ReqItems.find(x => x.ItemCount > 0)) {
// This research is now fully funded.
const recipe = ExportDojoRecipes.research[data.RecipeType];
processFundedProject(guild, techProject, recipe);
if (data.RecipeType.substring(0, 39) == "/Lotus/Types/Items/Research/DojoColors/") {
guild.ActiveDojoColorResearch = "";
await removePigmentsFromGuildMembers(guild._id);
}
}
await guild.save();
await inventory.save();
res.json({
InventoryChanges: inventoryChanges,
Vault: getGuildVault(guild)
});
} else if (data.Action.split(",")[0] == "Buy") {
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))) {
res.status(400).send("-1").end();
return;
}
const purchase = data as IGuildTechBuyRequest;
const quantity = parseInt(data.Action.split(",")[1]);
const recipeChanges = [
{
ItemType: purchase.RecipeType,
ItemCount: quantity
}
];
addRecipes(inventory, recipeChanges);
const currencyChanges = updateCurrency(
inventory,
ExportDojoRecipes.research[purchase.RecipeType].replicatePrice,
false
);
await inventory.save();
// Not a mistake: This response uses `inventoryChanges` instead of `InventoryChanges`.
res.json({
inventoryChanges: {
...currencyChanges,
Recipes: recipeChanges
}
});
} else if (data.Action == "Fabricate") {
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))) {
res.status(400).send("-1").end();
return;
}
const recipe = ExportDojoRecipes.fabrications[data.RecipeType];
const inventoryChanges: IInventoryChanges = updateCurrency(inventory, recipe.price, false);
inventoryChanges.MiscItems = recipe.ingredients.map(x => ({
ItemType: x.ItemType,
ItemCount: x.ItemCount * -1
}));
addMiscItems(inventory, inventoryChanges.MiscItems);
combineInventoryChanges(inventoryChanges, await addItem(inventory, recipe.resultType));
await inventory.save();
// Not a mistake: This response uses `inventoryChanges` instead of `InventoryChanges`.
res.json({ inventoryChanges: inventoryChanges });
} else if (data.Action == "Pause") {
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
res.status(400).send("-1").end();
return;
}
const project = guild.TechProjects!.find(x => x.ItemType == data.RecipeType)!;
project.State = -2;
guild.ActiveDojoColorResearch = "";
await guild.save();
await removePigmentsFromGuildMembers(guild._id);
res.end();
} else if (data.Action == "Unpause") {
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
res.status(400).send("-1").end();
return;
}
const project = guild.TechProjects!.find(x => x.ItemType == data.RecipeType)!;
project.State = 0;
guild.ActiveDojoColorResearch = data.RecipeType;
await guild.save();
res.end();
} else {
logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
throw new Error(`unknown guildTech action: ${data.Action}`);
}
};
const processFundedProject = (
guild: TGuildDatabaseDocument,
techProject: ITechProjectDatabase,
recipe: IDojoResearch
): void => {
techProject.State = 1;
techProject.CompletionDate = new Date(Date.now() + (config.noDojoResearchTime ? 0 : recipe.time) * 1000);
if (recipe.guildXpValue) {
guild.XP += recipe.guildXpValue;
}
setTechLogState(guild, techProject.ItemType, config.noDojoResearchTime ? 4 : 3, techProject.CompletionDate);
};
const setTechLogState = (
guild: TGuildDatabaseDocument,
type: string,
state: number,
dateTime: Date | undefined = undefined
): boolean => {
guild.TechChanges ??= [];
const entry = guild.TechChanges.find(x => x.details == type);
if (entry) {
if (entry.entryType == state) {
return false;
}
entry.dateTime = dateTime;
entry.entryType = state;
} else {
guild.TechChanges.push({
dateTime: dateTime,
entryType: state,
details: type
});
}
return true;
};
type TGuildTechRequest =
| { Action: "Sync" | "SomethingElseThatWeMightNotKnowAbout" }
| IGuildTechBasicRequest
| IGuildTechContributeRequest;
interface IGuildTechBasicRequest {
Action: "Start" | "Fabricate" | "Pause" | "Unpause";
Mode: "Guild";
RecipeType: string;
}
interface IGuildTechBuyRequest {
Action: string;
Mode: "Guild";
RecipeType: string;
}
interface IGuildTechContributeRequest {
Action: "Contribute";
ResearchId: "";
RecipeType: string;
RegularCredits: number;
MiscItems: IMiscItem[];
VaultCredits: number;
VaultMiscItems: IMiscItem[];
}

View File

@ -4,7 +4,6 @@ import { createNewSession } from "@/src/managers/sessionManager";
import { logger } from "@/src/utils/logger";
import { ISession } from "@/src/types/session";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
const hostSessionController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const hostSessionRequest = JSON.parse(req.body as string) as ISession;

View File

@ -1,8 +1,87 @@
import { RequestHandler } from "express";
import inbox from "@/static/fixed_responses/inbox.json";
import { Inbox } from "@/src/models/inboxModel";
import {
createNewEventMessages,
deleteAllMessagesRead,
deleteMessageRead,
getAllMessagesSorted,
getMessage
} from "@/src/services/inboxService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { addItems, getInventory } from "@/src/services/inventoryService";
import { logger } from "@/src/utils/logger";
import { ExportGear } from "warframe-public-export-plus";
const inboxController: RequestHandler = (_req, res) => {
res.json(inbox);
export const inboxController: RequestHandler = async (req, res) => {
const { deleteId, lastMessage: latestClientMessageId, messageId } = req.query;
const accountId = await getAccountIdForRequest(req);
if (deleteId) {
if (deleteId === "DeleteAllRead") {
await deleteAllMessagesRead(accountId);
res.status(200).end();
return;
}
await deleteMessageRead(deleteId as string);
res.status(200).end();
} else if (messageId) {
const message = await getMessage(messageId as string);
message.r = true;
const attachmentItems = message.att;
const attachmentCountedItems = message.countedAtt;
if (!attachmentItems && !attachmentCountedItems) {
await message.save();
res.status(200).end();
return;
}
const inventory = await getInventory(accountId);
const inventoryChanges = {};
if (attachmentItems) {
await addItems(
inventory,
attachmentItems.map(attItem => ({
ItemType: attItem,
ItemCount: attItem in ExportGear ? (ExportGear[attItem].purchaseQuantity ?? 1) : 1
})),
inventoryChanges
);
}
if (attachmentCountedItems) {
await addItems(inventory, attachmentCountedItems, inventoryChanges);
}
await inventory.save();
await message.save();
res.json({ InventoryChanges: inventoryChanges });
} else if (latestClientMessageId) {
await createNewEventMessages(req);
const messages = await Inbox.find({ ownerId: accountId }).sort({ date: 1 });
const latestClientMessage = messages.find(m => m._id.toString() === latestClientMessageId);
if (!latestClientMessage) {
logger.debug(`this should only happen after DeleteAllRead `);
res.json({ Inbox: messages });
return;
}
const newMessages = messages.filter(m => m.date > latestClientMessage.date);
if (newMessages.length === 0) {
res.send("no-new");
return;
}
res.json({ Inbox: newMessages });
} else {
//newly created event messages must be newer than account.LatestEventMessageDate
await createNewEventMessages(req);
const messages = await getAllMessagesSorted(accountId);
const inbox = messages.map(m => m.toJSON());
res.json({ Inbox: inbox });
}
};
export { inboxController };

View File

@ -1,16 +1,32 @@
import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory, addMiscItems } from "@/src/services/inventoryService";
import { getInventory, addMiscItems, updateCurrency, addRecipes, freeUpSlot } from "@/src/services/inventoryService";
import { IOid } from "@/src/types/commonTypes";
import {
IConsumedSuit,
IHelminthFoodRecord,
IInfestedFoundryClient,
IInfestedFoundryDatabase,
IInventoryClient,
IMiscItem,
InventorySlot,
ITypeCount
} from "@/src/types/inventoryTypes/inventoryTypes";
import { ExportMisc, ExportRecipes } from "warframe-public-export-plus";
import { getRecipe } from "@/src/services/itemDataService";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { toMongoDate } from "@/src/helpers/inventoryHelpers";
import { logger } from "@/src/utils/logger";
import { colorToShard } from "@/src/helpers/shardHelper";
import { config } from "@/src/services/configService";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const infestedFoundryController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
switch (req.query.mode) {
case "s": {
// shard installation
const request = getJSONfromString(String(req.body)) as IShardInstallRequest;
const request = getJSONfromString<IShardInstallRequest>(String(req.body));
const inventory = await getInventory(accountId);
const suit = inventory.Suits.find(suit => suit._id.toString() == request.SuitId.$oid)!;
if (!suit.ArchonCrystalUpgrades || suit.ArchonCrystalUpgrades.length != 5) {
@ -36,9 +52,52 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
break;
}
case "x": {
// shard removal
const request = getJSONfromString<IShardUninstallRequest>(String(req.body));
const inventory = await getInventory(accountId);
const suit = inventory.Suits.find(suit => suit._id.toString() == request.SuitId.$oid)!;
const miscItemChanges: IMiscItem[] = [];
if (suit.ArchonCrystalUpgrades![request.Slot].Color) {
// refund shard
const shard = Object.entries(colorToShard).find(
([color]) => color == suit.ArchonCrystalUpgrades![request.Slot].Color
)![1];
miscItemChanges.push({
ItemType: shard,
ItemCount: 1
});
addMiscItems(inventory, miscItemChanges);
}
// 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!;
applyCheatsToInfestedFoundry(infestedFoundry);
res.json({
InventoryChanges: {
MiscItems: miscItemChanges,
InfestedFoundry: infestedFoundry
}
});
break;
}
case "n": {
// name the beast
const request = getJSONfromString(String(req.body)) as IHelminthNameRequest;
const request = getJSONfromString<IHelminthNameRequest>(String(req.body));
const inventory = await getInventory(accountId);
inventory.InfestedFoundry ??= {};
inventory.InfestedFoundry.Name = request.newName;
@ -53,13 +112,251 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
break;
}
case "o": // offerings update
// {"OfferingsIndex":540,"SuitTypes":["/Lotus/Powersuits/PaxDuviricus/PaxDuviricusBaseSuit","/Lotus/Powersuits/Nezha/NezhaBaseSuit","/Lotus/Powersuits/Devourer/DevourerBaseSuit"],"Extra":false}
res.status(404).end();
case "c": {
// consume items
if (config.infiniteHelminthMaterials) {
res.status(400).end();
return;
}
const request = getJSONfromString<IHelminthFeedRequest>(String(req.body));
const inventory = await getInventory(accountId);
inventory.InfestedFoundry ??= {};
inventory.InfestedFoundry.Resources ??= [];
const miscItemChanges: IMiscItem[] = [];
let totalPercentagePointsGained = 0;
const currentUnixSeconds = Math.trunc(Date.now() / 1000);
for (const contribution of request.ResourceContributions) {
const snack = ExportMisc.helminthSnacks[contribution.ItemType];
// tally items for removal
const change = miscItemChanges.find(x => x.ItemType == contribution.ItemType);
if (change) {
change.ItemCount -= snack.count;
} else {
miscItemChanges.push({ ItemType: contribution.ItemType, ItemCount: snack.count * -1 });
}
if (snack.type == "/Lotus/Types/Items/InfestedFoundry/HelminthAppetiteCooldownReducer") {
// sentinent apetite
let mostDislikedSnackRecord: IHelminthFoodRecord = { ItemType: "", Date: 0 };
for (const resource of inventory.InfestedFoundry.Resources) {
if (resource.RecentlyConvertedResources) {
for (const record of resource.RecentlyConvertedResources) {
if (record.Date > mostDislikedSnackRecord.Date) {
mostDislikedSnackRecord = record;
}
}
}
}
logger.debug("helminth eats sentient resource; most disliked snack:", {
type: mostDislikedSnackRecord.ItemType,
date: mostDislikedSnackRecord.Date
});
mostDislikedSnackRecord.Date = currentUnixSeconds + 24 * 60 * 60; // Possibly unfaithful
continue;
}
let resource = inventory.InfestedFoundry.Resources.find(x => x.ItemType == snack.type);
if (!resource) {
resource =
inventory.InfestedFoundry.Resources[
inventory.InfestedFoundry.Resources.push({ ItemType: snack.type, Count: 0 }) - 1
];
}
resource.RecentlyConvertedResources ??= [];
let record = resource.RecentlyConvertedResources.find(x => x.ItemType == contribution.ItemType);
if (!record) {
record =
resource.RecentlyConvertedResources[
resource.RecentlyConvertedResources.push({ ItemType: contribution.ItemType, Date: 0 }) - 1
];
}
const hoursRemaining = (record.Date - currentUnixSeconds) / 3600;
const apetiteFactor = apetiteModel(hoursRemaining) / 30;
logger.debug(`helminth eating ${contribution.ItemType} (+${(snack.gain * 100).toFixed(0)}%)`, {
hoursRemaining,
apetiteFactor
});
if (hoursRemaining >= 18) {
record.Date = currentUnixSeconds + 72 * 60 * 60; // Possibly unfaithful
} else {
record.Date = currentUnixSeconds + 24 * 60 * 60;
}
totalPercentagePointsGained += snack.gain * 100 * apetiteFactor; // 30% would be gain=0.3, so percentage points is equal to gain * 100.
resource.Count += Math.trunc(snack.gain * 1000 * apetiteFactor); // 30% would be gain=0.3 or Count=300, so Count=gain*1000.
if (resource.Count > 1000) resource.Count = 1000;
}
const recipeChanges = addInfestedFoundryXP(inventory.InfestedFoundry, 666 * totalPercentagePointsGained);
addRecipes(inventory, recipeChanges);
addMiscItems(inventory, miscItemChanges);
await inventory.save();
res.json({
InventoryChanges: {
Recipes: recipeChanges,
InfestedFoundry: {
XP: inventory.InfestedFoundry.XP,
Resources: inventory.InfestedFoundry.Resources,
Slots: inventory.InfestedFoundry.Slots
},
MiscItems: miscItemChanges
}
});
break;
}
case "o": {
// offerings update
const request = getJSONfromString<IHelminthOfferingsUpdate>(String(req.body));
const inventory = await getInventory(accountId);
inventory.InfestedFoundry ??= {};
inventory.InfestedFoundry.InvigorationIndex = request.OfferingsIndex;
inventory.InfestedFoundry.InvigorationSuitOfferings = request.SuitTypes;
if (request.Extra) {
inventory.InfestedFoundry.InvigorationsApplied = 0;
}
await inventory.save();
const infestedFoundry = inventory.toJSON<IInventoryClient>().InfestedFoundry!;
applyCheatsToInfestedFoundry(infestedFoundry);
res.json({
InventoryChanges: {
InfestedFoundry: infestedFoundry
}
});
break;
}
case "a": {
// subsume warframe
const request = getJSONfromString<IHelminthSubsumeRequest>(String(req.body));
const inventory = await getInventory(accountId);
const recipe = getRecipe(request.Recipe)!;
if (!config.infiniteHelminthMaterials) {
for (const ingredient of recipe.secretIngredients!) {
const resource = inventory.InfestedFoundry!.Resources!.find(x => x.ItemType == ingredient.ItemType);
if (resource) {
resource.Count -= ingredient.ItemCount;
}
}
}
const suit = inventory.Suits.id(request.SuitId.$oid)!;
inventory.Suits.pull(suit);
const consumedSuit: IConsumedSuit = { s: suit.ItemType };
if (suit.Configs[0] && suit.Configs[0].pricol) {
consumedSuit.c = suit.Configs[0].pricol;
}
if ((inventory.InfestedFoundry!.XP ?? 0) < 73125_00) {
inventory.InfestedFoundry!.Slots!--;
}
inventory.InfestedFoundry!.ConsumedSuits ??= [];
inventory.InfestedFoundry!.ConsumedSuits.push(consumedSuit);
inventory.InfestedFoundry!.LastConsumedSuit = suit;
inventory.InfestedFoundry!.AbilityOverrideUnlockCooldown = new Date(Date.now() + 24 * 60 * 60 * 1000);
const recipeChanges = addInfestedFoundryXP(inventory.InfestedFoundry!, 1600_00);
addRecipes(inventory, recipeChanges);
freeUpSlot(inventory, InventorySlot.SUITS);
await inventory.save();
const infestedFoundry = inventory.toJSON<IInventoryClient>().InfestedFoundry!;
applyCheatsToInfestedFoundry(infestedFoundry);
res.json({
InventoryChanges: {
Recipes: recipeChanges,
RemovedIdItems: [
{
ItemId: request.SuitId
}
],
SuitBin: {
count: -1,
platinum: 0,
Slots: 1
},
InfestedFoundry: infestedFoundry
}
});
break;
}
case "r": {
// rush subsume
const inventory = await getInventory(accountId);
const currencyChanges = updateCurrency(inventory, 50, true);
const recipeChanges = handleSubsumeCompletion(inventory);
await inventory.save();
const infestedFoundry = inventory.toJSON<IInventoryClient>().InfestedFoundry!;
applyCheatsToInfestedFoundry(infestedFoundry);
res.json({
InventoryChanges: {
...currencyChanges,
Recipes: recipeChanges,
InfestedFoundry: infestedFoundry
}
});
break;
}
case "u": {
const request = getJSONfromString<IHelminthInvigorationRequest>(String(req.body));
const inventory = await getInventory(accountId);
const suit = inventory.Suits.id(request.SuitId.$oid)!;
const upgradesExpiry = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
suit.OffensiveUpgrade = request.OffensiveUpgradeType;
suit.DefensiveUpgrade = request.DefensiveUpgradeType;
suit.UpgradesExpiry = upgradesExpiry;
const recipeChanges = addInfestedFoundryXP(inventory.InfestedFoundry!, 4800_00);
addRecipes(inventory, recipeChanges);
if (!config.infiniteHelminthMaterials) {
for (let i = 0; i != request.ResourceTypes.length; ++i) {
inventory.InfestedFoundry!.Resources!.find(x => x.ItemType == request.ResourceTypes[i])!.Count -=
request.ResourceCosts[i];
}
}
inventory.InfestedFoundry!.InvigorationsApplied ??= 0;
inventory.InfestedFoundry!.InvigorationsApplied += 1;
await inventory.save();
const infestedFoundry = inventory.toJSON<IInventoryClient>().InfestedFoundry!;
applyCheatsToInfestedFoundry(infestedFoundry);
res.json({
SuitId: request.SuitId,
OffensiveUpgrade: request.OffensiveUpgradeType,
DefensiveUpgrade: request.DefensiveUpgradeType,
UpgradesExpiry: toMongoDate(upgradesExpiry),
InventoryChanges: {
Recipes: recipeChanges,
InfestedFoundry: infestedFoundry
}
});
break;
}
case "custom_unlockall": {
const inventory = await getInventory(accountId);
inventory.InfestedFoundry ??= {};
inventory.InfestedFoundry.XP ??= 0;
if (151875_00 > inventory.InfestedFoundry.XP) {
const recipeChanges = addInfestedFoundryXP(
inventory.InfestedFoundry,
151875_00 - inventory.InfestedFoundry.XP
);
addRecipes(inventory, recipeChanges);
await inventory.save();
}
res.end();
break;
}
default:
throw new Error(`unhandled infestedFoundry mode: ${req.query.mode}`);
logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
throw new Error(`unhandled infestedFoundry mode: ${String(req.query.mode)}`);
}
};
@ -70,21 +367,179 @@ interface IShardInstallRequest {
Color: string;
}
interface IShardUninstallRequest {
SuitId: IOid;
Slot: number;
}
interface IHelminthNameRequest {
newName: string;
}
const colorToShard: Record<string, string> = {
ACC_RED: "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalAmar",
ACC_RED_MYTHIC: "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalAmarMythic",
ACC_YELLOW: "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalNira",
ACC_YELLOW_MYTHIC: "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalNiraMythic",
ACC_BLUE: "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalBoreal",
ACC_BLUE_MYTHIC: "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalBorealMythic",
ACC_GREEN: "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalGreen",
ACC_GREEN_MYTHIC: "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalGreenMythic",
ACC_ORANGE: "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalOrange",
ACC_ORANGE_MYTHIC: "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalOrangeMythic",
ACC_PURPLE: "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalViolet",
ACC_PURPLE_MYTHIC: "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalVioletMythic"
interface IHelminthFeedRequest {
ResourceContributions: {
ItemType: string;
Date: number; // unix timestamp
}[];
}
export const addInfestedFoundryXP = (infestedFoundry: IInfestedFoundryDatabase, delta: number): ITypeCount[] => {
const recipeChanges: ITypeCount[] = [];
infestedFoundry.XP ??= 0;
const prevXP = infestedFoundry.XP;
infestedFoundry.XP += delta;
if (prevXP < 2250_00 && infestedFoundry.XP >= 2250_00) {
infestedFoundry.Slots ??= 0;
infestedFoundry.Slots += 3;
}
if (prevXP < 5625_00 && infestedFoundry.XP >= 5625_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthShieldsBlueprint",
ItemCount: 1
});
}
if (prevXP < 10125_00 && infestedFoundry.XP >= 10125_00) {
recipeChanges.push({ ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthHackBlueprint", ItemCount: 1 });
}
if (prevXP < 15750_00 && infestedFoundry.XP >= 15750_00) {
infestedFoundry.Slots ??= 0;
infestedFoundry.Slots += 10;
}
if (prevXP < 22500_00 && infestedFoundry.XP >= 22500_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthAmmoEfficiencyBlueprint",
ItemCount: 1
});
}
if (prevXP < 30375_00 && infestedFoundry.XP >= 30375_00) {
recipeChanges.push({ ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthStunBlueprint", ItemCount: 1 });
}
if (prevXP < 39375_00 && infestedFoundry.XP >= 39375_00) {
infestedFoundry.Slots ??= 0;
infestedFoundry.Slots += 20;
}
if (prevXP < 60750_00 && infestedFoundry.XP >= 60750_00) {
recipeChanges.push({ ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthStatusBlueprint", ItemCount: 1 });
}
if (prevXP < 73125_00 && infestedFoundry.XP >= 73125_00) {
infestedFoundry.Slots = 1;
}
if (prevXP < 86625_00 && infestedFoundry.XP >= 86625_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthShieldArmorBlueprint",
ItemCount: 1
});
}
if (prevXP < 101250_00 && infestedFoundry.XP >= 101250_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthProcBlockBlueprint",
ItemCount: 1
});
}
if (prevXP < 117000_00 && infestedFoundry.XP >= 117000_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthEnergyShareBlueprint",
ItemCount: 1
});
}
if (prevXP < 133875_00 && infestedFoundry.XP >= 133875_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthMaxStatusBlueprint",
ItemCount: 1
});
}
if (prevXP < 151875_00 && infestedFoundry.XP >= 151875_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthTreasureBlueprint",
ItemCount: 1
});
}
return recipeChanges;
};
interface IHelminthSubsumeRequest {
SuitId: IOid;
Recipe: string;
}
export const handleSubsumeCompletion = (inventory: TInventoryDatabaseDocument): ITypeCount[] => {
const [recipeType] = Object.entries(ExportRecipes).find(
([_recipeType, recipe]) =>
recipe.secretIngredientAction == "SIA_WARFRAME_ABILITY" &&
recipe.secretIngredients![0].ItemType == inventory.InfestedFoundry!.LastConsumedSuit!.ItemType
)!;
inventory.InfestedFoundry!.LastConsumedSuit = undefined;
inventory.InfestedFoundry!.AbilityOverrideUnlockCooldown = undefined;
const recipeChanges: ITypeCount[] = [
{
ItemType: recipeType,
ItemCount: 1
}
];
addRecipes(inventory, recipeChanges);
return recipeChanges;
};
export const applyCheatsToInfestedFoundry = (infestedFoundry: IInfestedFoundryClient): void => {
if (config.infiniteHelminthMaterials) {
infestedFoundry.Resources = [
{ ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthCalx", Count: 1000 },
{ ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthBiotics", Count: 1000 },
{ ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthSynthetics", Count: 1000 },
{ ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthPheromones", Count: 1000 },
{ ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthBile", Count: 1000 },
{ ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthOxides", Count: 1000 }
];
}
};
interface IHelminthOfferingsUpdate {
OfferingsIndex: number;
SuitTypes: string[];
Extra: boolean;
}
interface IHelminthInvigorationRequest {
SuitId: IOid;
OffensiveUpgradeType: string;
DefensiveUpgradeType: string;
ResourceTypes: string[];
ResourceCosts: number[];
}
// A fitted model for observed apetite values. Likely slightly inaccurate.
//
// Hours remaining, percentage points gained (out of 30 total)
// 0, 30
// 5, 25.8
// 10, 21.6
// 12, 20
// 16, 16.6
// 17, 15.8
// 18, 15
// 20, 15
// 24, 15
// 36, 15
// 40, 13.6
// 47, 11.3
// 48, 11
// 50, 10.3
// 60, 7
// 70, 3.6
// 71, 3.3
// 72, 3
const apetiteModel = (x: number): number => {
if (x <= 0) {
return 30;
}
if (x < 18) {
return -0.84 * x + 30;
}
if (x <= 36) {
return 15;
}
if (x < 71.9) {
return -0.3327892 * x + 26.94135;
}
return 3;
};

View File

@ -1,46 +1,120 @@
import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { toInventoryResponse } from "@/src/helpers/inventoryHelpers";
import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
import { Inventory, TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { config } from "@/src/services/configService";
import allDialogue from "@/static/fixed_responses/allDialogue.json";
import allMissions from "@/static/fixed_responses/allMissions.json";
import { ILoadoutDatabase } from "@/src/types/saveLoadoutTypes";
import { IInventoryDatabase, IShipInventory, equipmentKeys } from "@/src/types/inventoryTypes/inventoryTypes";
import { IPolarity, ArtifactPolarity } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { ExportCustoms, ExportFlavour, ExportKeys, ExportResources } from "warframe-public-export-plus";
import { IInventoryClient, IShipInventory, equipmentKeys } from "@/src/types/inventoryTypes/inventoryTypes";
import { IPolarity, ArtifactPolarity, EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes";
import {
ExportCustoms,
ExportFlavour,
ExportRegions,
ExportResources,
ExportVirtuals
} from "warframe-public-export-plus";
import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "./infestedFoundryController";
import { addMiscItems, allDailyAffiliationKeys, createLibraryDailyTask } from "@/src/services/inventoryService";
import { logger } from "@/src/utils/logger";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
const inventoryController: RequestHandler = async (request, response) => {
let accountId;
try {
accountId = await getAccountIdForRequest(request);
} catch (e) {
response.status(400).send("Log-in expired");
return;
}
export const inventoryController: RequestHandler = async (request, response) => {
const accountId = await getAccountIdForRequest(request);
const inventory = await Inventory.findOne({ accountOwnerId: accountId })
.populate<{ LoadOutPresets: ILoadoutDatabase }>("LoadOutPresets")
.populate<{ Ships: IShipInventory }>("Ships", "-ShipInteriorColors");
const inventory = await Inventory.findOne({ accountOwnerId: accountId });
if (!inventory) {
response.status(400).json({ error: "inventory was undefined" });
return;
}
//TODO: make a function that converts from database representation to client
const inventoryJSON: IInventoryDatabase = inventory.toJSON();
console.log(inventoryJSON.Ships);
// Handle daily reset
if (!inventory.NextRefill || Date.now() >= inventory.NextRefill.getTime()) {
for (const key of allDailyAffiliationKeys) {
inventory[key] = 16000 + inventory.PlayerLevel * 500;
}
inventory.DailyFocus = 250000 + inventory.PlayerLevel * 5000;
const inventoryResponse = toInventoryResponse(inventoryJSON);
inventory.LibraryAvailableDailyTaskInfo = createLibraryDailyTask();
if (config.infiniteResources) {
if (inventory.NextRefill) {
if (config.noArgonCrystalDecay) {
inventory.FoundToday = undefined;
} else {
const lastLoginDay = Math.trunc(inventory.NextRefill.getTime() / 86400000) - 1;
const today = Math.trunc(Date.now() / 86400000);
const daysPassed = today - lastLoginDay;
for (let i = 0; i != daysPassed; ++i) {
const numArgonCrystals =
inventory.MiscItems.find(x => x.ItemType == "/Lotus/Types/Items/MiscItems/ArgonCrystal")
?.ItemCount ?? 0;
if (numArgonCrystals == 0) {
break;
}
const numStableArgonCrystals =
inventory.FoundToday?.find(x => x.ItemType == "/Lotus/Types/Items/MiscItems/ArgonCrystal")
?.ItemCount ?? 0;
const numDecayingArgonCrystals = numArgonCrystals - numStableArgonCrystals;
const numDecayingArgonCrystalsToRemove = Math.ceil(numDecayingArgonCrystals / 2);
logger.debug(`ticking argon crystals for day ${i + 1} of ${daysPassed}`, {
numArgonCrystals,
numStableArgonCrystals,
numDecayingArgonCrystals,
numDecayingArgonCrystalsToRemove
});
// Remove half of owned decaying argon crystals
addMiscItems(inventory, [
{
ItemType: "/Lotus/Types/Items/MiscItems/ArgonCrystal",
ItemCount: numDecayingArgonCrystalsToRemove * -1
}
]);
// All stable argon crystals are now decaying
inventory.FoundToday = undefined;
}
}
}
inventory.NextRefill = new Date((Math.trunc(Date.now() / 86400000) + 1) * 86400000);
await inventory.save();
}
if (
inventory.InfestedFoundry &&
inventory.InfestedFoundry.AbilityOverrideUnlockCooldown &&
new Date() >= inventory.InfestedFoundry.AbilityOverrideUnlockCooldown
) {
handleSubsumeCompletion(inventory);
await inventory.save();
}
response.json(await getInventoryResponse(inventory, "xpBasedLevelCapDisabled" in request.query));
};
export const getInventoryResponse = async (
inventory: TInventoryDatabaseDocument,
xpBasedLevelCapDisabled: boolean
): Promise<IInventoryClient> => {
const inventoryWithLoadOutPresets = await inventory.populate<{ LoadOutPresets: ILoadoutDatabase }>(
"LoadOutPresets"
);
const inventoryWithLoadOutPresetsAndShips = await inventoryWithLoadOutPresets.populate<{ Ships: IShipInventory }>(
"Ships"
);
const inventoryResponse = inventoryWithLoadOutPresetsAndShips.toJSON<IInventoryClient>();
if (config.infiniteCredits) {
inventoryResponse.RegularCredits = 999999999;
inventoryResponse.TradesRemaining = 999999999;
inventoryResponse.PremiumCreditsFree = 999999999;
}
if (config.infinitePlatinum) {
inventoryResponse.PremiumCreditsFree = 0;
inventoryResponse.PremiumCredits = 999999999;
}
if (config.infiniteEndo) {
inventoryResponse.FusionPoints = 999999999;
}
if (config.infiniteRegalAya) {
inventoryResponse.PrimeTokens = 999999999;
}
if (config.skipAllDialogue) {
inventoryResponse.TauntHistory = [
@ -55,38 +129,17 @@ const inventoryController: RequestHandler = async (request, response) => {
}
if (config.unlockAllMissions) {
inventoryResponse.Missions = allMissions;
inventoryResponse.Missions = [];
for (const tag of Object.keys(ExportRegions)) {
inventoryResponse.Missions.push({
Completes: 1,
Tier: 1,
Tag: tag
});
}
addString(inventoryResponse.NodeIntrosCompleted, "TeshinHardModeUnlocked");
}
if (config.unlockAllQuests) {
for (const [k, v] of Object.entries(ExportKeys)) {
if ("chainStages" in v) {
if (!inventoryResponse.QuestKeys.find(quest => quest.ItemType == k)) {
inventoryResponse.QuestKeys.push({ ItemType: k });
}
}
}
}
if (config.completeAllQuests) {
for (const quest of inventoryResponse.QuestKeys) {
quest.Completed = true;
quest.Progress = [
{
c: 0,
i: false,
m: false,
b: []
}
];
}
inventoryResponse.ArchwingEnabled = true;
// Skip "Watch The Maker"
addString(inventoryResponse.NodeIntrosCompleted, "/Lotus/Levels/Cinematics/NewWarIntro/NewWarStageTwo.level");
}
if (config.unlockAllShipDecorations) {
inventoryResponse.ShipDecorations = [];
for (const [uniqueName, item] of Object.entries(ExportResources)) {
@ -104,20 +157,32 @@ const inventoryController: RequestHandler = async (request, response) => {
}
if (config.unlockAllSkins) {
inventoryResponse.WeaponSkins = [];
for (const uniqueName in ExportCustoms) {
const missingWeaponSkins = new Set(Object.keys(ExportCustoms));
inventoryResponse.WeaponSkins.forEach(x => missingWeaponSkins.delete(x.ItemType));
for (const uniqueName of missingWeaponSkins) {
inventoryResponse.WeaponSkins.push({
ItemId: {
$oid: "000000000000000000000000"
$oid: "ca70ca70ca70ca70" + catBreadHash(uniqueName).toString(16).padStart(8, "0")
},
ItemType: uniqueName
});
}
}
if (config.unlockAllCapturaScenes) {
for (const uniqueName of Object.keys(ExportResources)) {
if (resourceInheritsFrom(uniqueName, "/Lotus/Types/Items/MiscItems/PhotoboothTile")) {
inventoryResponse.MiscItems.push({
ItemType: uniqueName,
ItemCount: 1
});
}
}
}
if (typeof config.spoofMasteryRank === "number" && config.spoofMasteryRank >= 0) {
inventoryResponse.PlayerLevel = config.spoofMasteryRank;
if (!("xpBasedLevelCapDisabled" in request.query)) {
if (!xpBasedLevelCapDisabled) {
// This client has not been patched to accept any mastery rank, need to fake the XP.
inventoryResponse.XPInfo = [];
let numFrames = getExpRequiredForMr(Math.min(config.spoofMasteryRank, 5030)) / 6000;
@ -132,7 +197,7 @@ const inventoryController: RequestHandler = async (request, response) => {
if (config.universalPolarityEverywhere) {
const Polarity: IPolarity[] = [];
for (let i = 0; i != 10; ++i) {
for (let i = 0; i != 12; ++i) {
Polarity.push({
Slot: i,
Value: ArtifactPolarity.Any
@ -147,13 +212,60 @@ const inventoryController: RequestHandler = async (request, response) => {
}
}
// Fix for #380
inventoryResponse.NextRefill = { $date: { $numberLong: "9999999999999" } };
if (config.unlockDoubleCapacityPotatoesEverywhere) {
for (const key of equipmentKeys) {
if (key in inventoryResponse) {
for (const equipment of inventoryResponse[key]) {
equipment.Features ??= 0;
equipment.Features |= EquipmentFeatures.DOUBLE_CAPACITY;
}
}
}
}
response.json(inventoryResponse);
if (config.unlockExilusEverywhere) {
for (const key of equipmentKeys) {
if (key in inventoryResponse) {
for (const equipment of inventoryResponse[key]) {
equipment.Features ??= 0;
equipment.Features |= EquipmentFeatures.UTILITY_SLOT;
}
}
}
}
if (config.unlockArcanesEverywhere) {
for (const key of equipmentKeys) {
if (key in inventoryResponse) {
for (const equipment of inventoryResponse[key]) {
equipment.Features ??= 0;
equipment.Features |= EquipmentFeatures.ARCANE_SLOT;
}
}
}
}
if (config.noDailyStandingLimits) {
const spoofedDailyAffiliation = Math.max(999_999, 16000 + inventoryResponse.PlayerLevel * 500);
for (const key of allDailyAffiliationKeys) {
inventoryResponse[key] = spoofedDailyAffiliation;
}
}
if (inventoryResponse.InfestedFoundry) {
applyCheatsToInfestedFoundry(inventoryResponse.InfestedFoundry);
}
// Omitting this field so opening the navigation resyncs the inventory which is more desirable for typical usage.
//inventoryResponse.LastInventorySync = toOid(new Types.ObjectId());
// Set 2FA enabled so trading post can be used
inventoryResponse.HWIDProtectEnabled = true;
return inventoryResponse;
};
const addString = (arr: string[], str: string): void => {
export const addString = (arr: string[], str: string): void => {
if (!arr.find(x => x == str)) {
arr.push(str);
}
@ -166,4 +278,30 @@ const getExpRequiredForMr = (rank: number): number => {
return 2_250_000 + 147_500 * (rank - 30);
};
export { inventoryController };
const resourceInheritsFrom = (resourceName: string, targetName: string): boolean => {
let parentName = resourceGetParent(resourceName);
for (; parentName != undefined; parentName = resourceGetParent(parentName)) {
if (parentName == targetName) {
return true;
}
}
return false;
};
const resourceGetParent = (resourceName: string): string | undefined => {
if (resourceName in ExportResources) {
return ExportResources[resourceName].parentName;
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
return ExportVirtuals[resourceName]?.parentName;
};
// This is FNV1a-32 except operating under modulus 2^31 because JavaScript is stinky and likes producing negative integers out of nowhere.
export const catBreadHash = (name: string): number => {
let hash = 2166136261;
for (let i = 0; i != name.length; ++i) {
hash = (hash ^ name.charCodeAt(i)) & 0x7fffffff;
hash = (hash * 16777619) & 0x7fffffff;
}
return hash;
};

View File

@ -1,8 +1,9 @@
import { getAccountIdForRequest } from "@/src/services/loginService";
import { updateCurrency } from "@/src/services/inventoryService";
import { getInventory, updateCurrency } from "@/src/services/inventoryService";
import { RequestHandler } from "express";
import { updateSlots } from "@/src/services/inventoryService";
import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger";
/*
loadout slots are additionally purchased slots only
@ -18,19 +19,22 @@ import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
number of frames = extra - slots + 2
*/
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const inventorySlotsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
//const body = JSON.parse(req.body as string) as IInventorySlotsRequest;
const body = JSON.parse(req.body as string) as IInventorySlotsRequest;
//console.log(body);
if (body.Bin != InventorySlot.SUITS && body.Bin != InventorySlot.PVE_LOADOUTS) {
logger.warn(`unexpected slot purchase of type ${body.Bin}, account may be overcharged`);
}
//TODO: check which slot was purchased because pvpBonus is also possible
const currencyChanges = await updateCurrency(20, true, accountId);
await updateSlots(accountId, InventorySlot.PVE_LOADOUTS, 1, 1);
//console.log({ InventoryChanges: currencyChanges }, " added loadout changes:");
const inventory = await getInventory(accountId);
const currencyChanges = updateCurrency(inventory, 20, true);
updateSlots(inventory, body.Bin, 1, 1);
await inventory.save();
res.json({ InventoryChanges: currencyChanges });
};
interface IInventorySlotsRequest {
Bin: InventorySlot;
}

View File

@ -2,12 +2,13 @@ import { RequestHandler } from "express";
import { getSessionByID } from "@/src/managers/sessionManager";
import { logger } from "@/src/utils/logger";
const joinSessionController: RequestHandler = (_req, res) => {
const reqBody = JSON.parse(String(_req.body));
export const joinSessionController: RequestHandler = (req, res) => {
const reqBody = JSON.parse(String(req.body)) as IJoinSessionRequest;
logger.debug(`JoinSession Request`, { reqBody });
const req = JSON.parse(String(_req.body));
const session = getSessionByID(req.sessionIds[0] as string);
const session = getSessionByID(reqBody.sessionIds[0]);
res.json({ rewardSeed: session?.rewardSeed, sessionId: { $oid: session?.sessionId } });
};
export { joinSessionController };
interface IJoinSessionRequest {
sessionIds: string[];
}

View File

@ -1,31 +1,41 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { RequestHandler } from "express";
import { config } from "@/src/services/configService";
import buildConfig from "@/static/data/buildConfig.json";
import { buildConfig } from "@/src/services/buildConfigService";
import { toLoginRequest } from "@/src/helpers/loginHelpers";
import { Account } from "@/src/models/loginModel";
import { createAccount, isCorrectPassword } from "@/src/services/loginService";
import { ILoginResponse } from "@/src/types/loginTypes";
import { DTLS, groups, HUB, platformCDNs } from "@/static/fixed_responses/login_static";
import { createAccount, isCorrectPassword, isNameTaken } from "@/src/services/loginService";
import { IDatabaseAccountJson, ILoginRequest, ILoginResponse } from "@/src/types/loginTypes";
import { logger } from "@/src/utils/logger";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
const loginController: RequestHandler = async (request, response) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-argument
const body = JSON.parse(request.body); // parse octet stream of json data to json object
const loginRequest = toLoginRequest(body);
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
const account = await Account.findOne({ email: loginRequest.email }); //{ _id: 0, __v: 0 }
const account = await Account.findOne({ email: loginRequest.email });
const nonce = Math.round(Math.random() * Number.MAX_SAFE_INTEGER);
const buildLabel: string =
typeof request.query.buildLabel == "string"
? request.query.buildLabel.split(" ").join("+")
: buildConfig.buildLabel;
const myAddress = request.host.indexOf("warframe.com") == -1 ? request.host : config.myAddress;
if (!account && config.autoCreateAccount && loginRequest.ClientType != "webui") {
try {
const nameFromEmail = loginRequest.email.substring(0, loginRequest.email.indexOf("@"));
let name = nameFromEmail;
if (await isNameTaken(name)) {
let suffix = 0;
do {
++suffix;
name = nameFromEmail + suffix;
} while (await isNameTaken(name));
}
const newAccount = await createAccount({
email: loginRequest.email,
password: loginRequest.password,
DisplayName: loginRequest.email.substring(0, loginRequest.email.indexOf("@")),
DisplayName: name,
CountryCode: loginRequest.lang.toUpperCase(),
ClientType: loginRequest.ClientType,
CrossPlatformAllowed: true,
@ -35,21 +45,7 @@ const loginController: RequestHandler = async (request, response) => {
Nonce: nonce
});
logger.debug("created new account");
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { email, password, ...databaseAccount } = newAccount;
const newLoginResponse: ILoginResponse = {
...databaseAccount,
Groups: groups,
platformCDNs: platformCDNs,
NRS: [config.myAddress],
DTLS: DTLS,
IRC: config.myIrcAddresses ?? [config.myAddress],
HUB: HUB,
BuildLabel: buildConfig.buildLabel,
MatchmakingBuildId: buildConfig.matchmakingBuildId
};
response.json(newLoginResponse);
response.json(createLoginResponse(myAddress, newAccount, buildLabel));
return;
} catch (error: unknown) {
if (error instanceof Error) {
@ -64,28 +60,46 @@ const loginController: RequestHandler = async (request, response) => {
return;
}
if (account.Nonce == 0 || loginRequest.ClientType != "webui") {
if (loginRequest.ClientType == "webui") {
if (!account.Nonce) {
account.ClientType = "webui";
account.Nonce = nonce;
}
} else {
if (account.Nonce && account.ClientType != "webui" && !account.Dropped && !loginRequest.kick) {
response.status(400).json({ error: "nonce still set" });
return;
}
account.ClientType = loginRequest.ClientType;
account.Nonce = nonce;
}
if (loginRequest.ClientType != "webui") {
account.CountryCode = loginRequest.lang.toUpperCase();
}
await account.save();
const { email, password, ...databaseAccount } = account.toJSON();
const newLoginResponse: ILoginResponse = {
...databaseAccount,
Groups: groups,
platformCDNs: platformCDNs,
NRS: [config.myAddress],
DTLS: DTLS,
IRC: config.myIrcAddresses ?? [config.myAddress],
HUB: HUB,
BuildLabel: buildConfig.buildLabel,
MatchmakingBuildId: buildConfig.matchmakingBuildId
};
response.json(newLoginResponse);
response.json(createLoginResponse(myAddress, account.toJSON(), buildLabel));
};
export { loginController };
const createLoginResponse = (myAddress: string, account: IDatabaseAccountJson, buildLabel: string): ILoginResponse => {
return {
id: account.id,
DisplayName: account.DisplayName,
CountryCode: account.CountryCode,
ClientType: account.ClientType,
CrossPlatformAllowed: account.CrossPlatformAllowed,
ForceLogoutVersion: account.ForceLogoutVersion,
AmazonAuthToken: account.AmazonAuthToken,
AmazonRefreshToken: account.AmazonRefreshToken,
ConsentNeeded: account.ConsentNeeded,
TrackedSettings: account.TrackedSettings,
Nonce: account.Nonce,
Groups: [],
IRC: config.myIrcAddresses ?? [myAddress],
platformCDNs: [`https://${myAddress}/`],
HUB: `https://${myAddress}/api/`,
NRS: config.NRS,
DTLS: 99,
BuildLabel: buildLabel,
MatchmakingBuildId: buildConfig.matchmakingBuildId
};
};

View File

@ -1,8 +1,40 @@
import { RequestHandler } from "express";
import loginRewards from "@/static/fixed_responses/loginRewards.json";
import { getAccountForRequest } from "@/src/services/loginService";
import { claimLoginReward, getRandomLoginRewards, ILoginRewardsReponse } from "@/src/services/loginRewardService";
import { getInventory } from "@/src/services/inventoryService";
const loginRewardsController: RequestHandler = (_req, res) => {
res.json(loginRewards);
export const loginRewardsController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req);
const today = Math.trunc(Date.now() / 86400000) * 86400;
if (today == account.LastLoginRewardDate) {
res.end();
return;
}
account.LoginDays += 1;
account.LastLoginRewardDate = today;
await account.save();
const inventory = await getInventory(account._id.toString());
const randomRewards = getRandomLoginRewards(account, inventory);
const isMilestoneDay = account.LoginDays == 5 || account.LoginDays % 50 == 0;
const response: ILoginRewardsReponse = {
DailyTributeInfo: {
Rewards: randomRewards,
IsMilestoneDay: isMilestoneDay,
IsChooseRewardSet: randomRewards.length != 1,
LoginDays: account.LoginDays,
//NextMilestoneReward: "",
NextMilestoneDay: account.LoginDays < 5 ? 5 : (Math.trunc(account.LoginDays / 50) + 1) * 50,
HasChosenReward: false
},
LastLoginRewardDate: today
};
if (!isMilestoneDay && randomRewards.length == 1) {
response.DailyTributeInfo.HasChosenReward = true;
response.DailyTributeInfo.ChosenReward = randomRewards[0];
response.DailyTributeInfo.NewInventory = await claimLoginReward(inventory, randomRewards[0]);
await inventory.save();
}
res.json(response);
};
export { loginRewardsController };

View File

@ -0,0 +1,57 @@
import { getInventory } from "@/src/services/inventoryService";
import { claimLoginReward, getRandomLoginRewards } from "@/src/services/loginRewardService";
import { getAccountForRequest } from "@/src/services/loginService";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express";
export const loginRewardsSelectionController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req);
const inventory = await getInventory(account._id.toString());
const body = JSON.parse(String(req.body)) as ILoginRewardsSelectionRequest;
const isMilestoneDay = account.LoginDays == 5 || account.LoginDays % 50 == 0;
if (body.IsMilestoneReward != isMilestoneDay) {
logger.warn(`Client disagrees on login milestone (got ${body.IsMilestoneReward}, expected ${isMilestoneDay})`);
}
let chosenReward;
let inventoryChanges: IInventoryChanges;
if (body.IsMilestoneReward) {
chosenReward = {
RewardType: "RT_STORE_ITEM",
StoreItemType: body.ChosenReward
};
inventoryChanges = (await handleStoreItemAcquisition(body.ChosenReward, inventory)).InventoryChanges;
if (!evergreenRewards.find(x => x == body.ChosenReward)) {
inventory.LoginMilestoneRewards.push(body.ChosenReward);
}
} else {
const randomRewards = getRandomLoginRewards(account, inventory);
chosenReward = randomRewards.find(x => x.StoreItemType == body.ChosenReward)!;
inventoryChanges = await claimLoginReward(inventory, chosenReward);
}
await inventory.save();
res.json({
DailyTributeInfo: {
NewInventory: inventoryChanges,
ChosenReward: chosenReward
}
});
};
interface ILoginRewardsSelectionRequest {
ChosenReward: string;
IsMilestoneReward: boolean;
}
const evergreenRewards = [
"/Lotus/Types/StoreItems/Packages/EvergreenTripleForma",
"/Lotus/Types/StoreItems/Packages/EvergreenTripleRifleRiven",
"/Lotus/Types/StoreItems/Packages/EvergreenTripleMeleeRiven",
"/Lotus/Types/StoreItems/Packages/EvergreenTripleSecondaryRiven",
"/Lotus/Types/StoreItems/Packages/EvergreenWeaponSlots",
"/Lotus/Types/StoreItems/Packages/EvergreenKuva",
"/Lotus/Types/StoreItems/Packages/EvergreenBoosters",
"/Lotus/Types/StoreItems/Packages/EvergreenEndo",
"/Lotus/Types/StoreItems/Packages/EvergreenExilus"
];

View File

@ -2,7 +2,6 @@ import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { Account } from "@/src/models/loginModel";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
const logoutController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const account = await Account.findOne({ _id: accountId });

View File

@ -1,10 +1,12 @@
import { RequestHandler } from "express";
import { missionInventoryUpdate } from "@/src/services/inventoryService";
import { combineRewardAndLootInventory, getRewards } from "@/src/services/missionInventoryUpdateService";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IMissionInventoryUpdateRequest } from "@/src/types/requestTypes";
import { addMissionInventoryUpdates, addMissionRewards } from "@/src/services/missionInventoryUpdateService";
import { getInventory } from "@/src/services/inventoryService";
import { getInventoryResponse } from "./inventoryController";
import { logger } from "@/src/utils/logger";
/*
**** INPUT ****
- [ ] crossPlaySetting
@ -30,13 +32,13 @@ import { logger } from "@/src/utils/logger";
- [ ] hosts
- [x] ChallengeProgress
- [ ] SeasonChallengeHistory
- [ ] PS (Passive anti-cheat data which includes your username, module list, process list, and system name.)
- [ ] PS (anticheat data)
- [ ] ActiveDojoColorResearch
- [x] RewardInfo
- [ ] ReceivedCeremonyMsg
- [ ] LastCeremonyResetDate
- [ ] MissionPTS (Used to validate the mission/alive time above.)
- [ ] RepHash (A hash from the replication manager/RepMgr Unknown what it does.)
- [ ] RepHash
- [ ] EndOfMatchUpload
- [ ] ObjectiveReached
- [ ] FpsAvg
@ -44,36 +46,40 @@ import { logger } from "@/src/utils/logger";
- [ ] FpsMax
- [ ] FpsSamples
*/
//move credit calc in here, return MissionRewards: [] if no reward info
// eslint-disable-next-line @typescript-eslint/no-misused-promises
const missionInventoryUpdateController: RequestHandler = async (req, res): Promise<void> => {
export const missionInventoryUpdateController: RequestHandler = async (req, res): Promise<void> => {
const accountId = await getAccountIdForRequest(req);
const missionReport = getJSONfromString<IMissionInventoryUpdateRequest>((req.body as string).toString());
logger.debug("mission report:", missionReport);
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call
const lootInventory = getJSONfromString(req.body.toString()) as IMissionInventoryUpdateRequest;
const inventory = await getInventory(accountId);
const inventoryUpdates = await addMissionInventoryUpdates(inventory, missionReport);
logger.debug("missionInventoryUpdate with lootInventory =", lootInventory);
const { InventoryChanges, MissionRewards } = getRewards(lootInventory);
const { combinedInventoryChanges, TotalCredits, CreditsBonus, MissionCredits, FusionPoints } =
combineRewardAndLootInventory(InventoryChanges, lootInventory);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const InventoryJson = JSON.stringify(await missionInventoryUpdate(combinedInventoryChanges, accountId));
if (missionReport.MissionStatus !== "GS_SUCCESS") {
await inventory.save();
const inventoryResponse = await getInventoryResponse(inventory, true);
res.json({
// InventoryJson, // this part will reset game data and missions will be locked
MissionRewards,
InventoryChanges,
TotalCredits,
CreditsBonus,
MissionCredits,
FusionPoints
InventoryJson: JSON.stringify(inventoryResponse),
MissionRewards: []
});
} catch (err) {
console.error("Error parsing JSON data:", err);
return;
}
const { MissionRewards, inventoryChanges, credits } = await addMissionRewards(inventory, missionReport);
await inventory.save();
const inventoryResponse = await getInventoryResponse(inventory, true);
//TODO: figure out when to send inventory. it is needed for many cases.
res.json({
InventoryJson: JSON.stringify(inventoryResponse),
InventoryChanges: inventoryChanges,
MissionRewards,
...credits,
...inventoryUpdates,
FusionPoints: inventoryChanges?.FusionPoints
});
};
/*
@ -86,5 +92,3 @@ const missionInventoryUpdateController: RequestHandler = async (req, res): Promi
- [x] InventoryChanges
- [x] FusionPoints
*/
export { missionInventoryUpdateController };

View File

@ -1,48 +1,44 @@
import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
import { getInventory, updateCurrency, addEquipment, addMiscItems } from "@/src/services/inventoryService";
const modularWeaponTypes: Record<string, TEquipmentKey> = {
"/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryBeam": "LongGuns",
"/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary": "Pistols",
"/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryBeam": "Pistols",
"/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryShotgun": "Pistols",
"/Lotus/Weapons/Ostron/Melee/LotusModularWeapon": "Melee",
"/Lotus/Weapons/Sentients/OperatorAmplifiers/OperatorAmpWeapon": "OperatorAmps",
"/Lotus/Types/Vehicles/Hoverboard/HoverboardSuit": "Hoverboards",
"/Lotus/Types/Friendly/Pets/MoaPets/MoaPetPowerSuit": "MoaPets",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetAPowerSuit": "MoaPets",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetBPowerSuit": "MoaPets",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetCPowerSuit": "MoaPets"
};
import {
getInventory,
updateCurrency,
addEquipment,
addMiscItems,
applyDefaultUpgrades,
occupySlot,
productCategoryToInventoryBin
} from "@/src/services/inventoryService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { getDefaultUpgrades } from "@/src/services/itemDataService";
import { modularWeaponTypes } from "@/src/helpers/modularWeaponHelper";
interface IModularCraftRequest {
WeaponType: string;
Parts: string[];
}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const modularWeaponCraftingController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const data = getJSONfromString(String(req.body)) as IModularCraftRequest;
const data = getJSONfromString<IModularCraftRequest>(String(req.body));
if (!(data.WeaponType in modularWeaponTypes)) {
throw new Error(`unknown modular weapon type: ${data.WeaponType}`);
}
const category = modularWeaponTypes[data.WeaponType];
const inventory = await getInventory(accountId);
// Give weapon
const weapon = await addEquipment(category, data.WeaponType, accountId, data.Parts);
const defaultUpgrades = getDefaultUpgrades(data.Parts);
const configs = applyDefaultUpgrades(inventory, defaultUpgrades);
const inventoryChanges: IInventoryChanges = {
...addEquipment(inventory, category, data.WeaponType, data.Parts, {}, { Configs: configs }),
...occupySlot(inventory, productCategoryToInventoryBin(category)!, false)
};
if (defaultUpgrades) {
inventoryChanges.RawUpgrades = defaultUpgrades.map(x => ({ ItemType: x.ItemType, ItemCount: 1 }));
}
// Remove credits
const currencyChanges = await updateCurrency(
category == "Hoverboards" || category == "MoaPets" ? 5000 : 4000,
false,
accountId
);
// Remove parts
// Remove credits & parts
const miscItemChanges = [];
for (const part of data.Parts) {
miscItemChanges.push({
@ -50,15 +46,21 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res)
ItemCount: -1
});
}
const inventory = await getInventory(accountId);
const currencyChanges = updateCurrency(
inventory,
category == "Hoverboards" || category == "MoaPets" || category == "LongGuns" || category == "Pistols"
? 5000
: 4000, // Definitely correct for Melee & OperatorAmps
false
);
addMiscItems(inventory, miscItemChanges);
await inventory.save();
// Tell client what we did
res.json({
InventoryChanges: {
...inventoryChanges,
...currencyChanges,
[category]: [weapon],
MiscItems: miscItemChanges
}
});

View File

@ -1,8 +1,190 @@
import { RequestHandler } from "express";
import modularWeaponSale from "@/static/fixed_responses/modularWeaponSale.json";
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 { ArtifactPolarity, EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import {
addEquipment,
applyDefaultUpgrades,
getInventory,
occupySlot,
productCategoryToInventoryBin,
updateCurrency
} from "@/src/services/inventoryService";
import { getDefaultUpgrades } from "@/src/services/itemDataService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { modularWeaponTypes } from "@/src/helpers/modularWeaponHelper";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
const modularWeaponSaleController: RequestHandler = (_req, res) => {
res.json(modularWeaponSale);
export const modularWeaponSaleController: RequestHandler = async (req, res) => {
const partTypeToParts: Record<string, string[]> = {};
for (const [uniqueName, data] of Object.entries(ExportWeapons)) {
if (data.partType) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
partTypeToParts[data.partType] ??= [];
partTypeToParts[data.partType].push(uniqueName);
}
}
if (req.query.op == "SyncAll") {
res.json({
SaleInfos: getSaleInfos(partTypeToParts, Math.trunc(Date.now() / 86400000))
});
} else if (req.query.op == "Purchase") {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const payload = getJSONfromString<IModularWeaponPurchaseRequest>(String(req.body));
const weaponInfo = getSaleInfos(partTypeToParts, payload.Revision).find(x => x.Name == payload.SaleName)!
.Weapons[payload.ItemIndex];
const category = modularWeaponTypes[weaponInfo.ItemType];
const defaultUpgrades = getDefaultUpgrades(weaponInfo.ModularParts);
const configs = applyDefaultUpgrades(inventory, defaultUpgrades);
const inventoryChanges: IInventoryChanges = {
...addEquipment(
inventory,
category,
weaponInfo.ItemType,
weaponInfo.ModularParts,
{},
{
Features: EquipmentFeatures.DOUBLE_CAPACITY | EquipmentFeatures.GILDED,
ItemName: payload.ItemName,
Configs: configs,
Polarity: [
{
Slot: payload.PolarizeSlot,
Value: payload.PolarizeValue
}
]
}
),
...occupySlot(inventory, productCategoryToInventoryBin(category)!, true),
...updateCurrency(inventory, weaponInfo.PremiumPrice, true)
};
if (defaultUpgrades) {
inventoryChanges.RawUpgrades = defaultUpgrades.map(x => ({ ItemType: x.ItemType, ItemCount: 1 }));
}
await inventory.save();
res.json({
InventoryChanges: inventoryChanges
});
} else {
throw new Error(`unknown modularWeaponSale op: ${String(req.query.op)}`);
}
};
export { modularWeaponSaleController };
const getSaleInfos = (partTypeToParts: Record<string, string[]>, day: number): IModularWeaponSaleInfo[] => {
const kitgunIsPrimary: boolean = (day & 1) != 0;
return [
getModularWeaponSale(
partTypeToParts,
day,
"Ostron",
["LWPT_HILT", "LWPT_BLADE", "LWPT_HILT_WEIGHT"],
() => "/Lotus/Weapons/Ostron/Melee/LotusModularWeapon"
),
getModularWeaponSale(
partTypeToParts,
day,
"SolarisUnitedHoverboard",
["LWPT_HB_DECK", "LWPT_HB_ENGINE", "LWPT_HB_FRONT", "LWPT_HB_JET"],
() => "/Lotus/Types/Vehicles/Hoverboard/HoverboardSuit"
),
getModularWeaponSale(
partTypeToParts,
day,
"SolarisUnitedMoaPet",
["LWPT_MOA_LEG", "LWPT_MOA_HEAD", "LWPT_MOA_ENGINE", "LWPT_MOA_PAYLOAD"],
() => "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetPowerSuit"
),
getModularWeaponSale(
partTypeToParts,
day,
"SolarisUnitedKitGun",
[
kitgunIsPrimary ? "LWPT_GUN_PRIMARY_HANDLE" : "LWPT_GUN_SECONDARY_HANDLE",
"LWPT_GUN_BARREL",
"LWPT_GUN_CLIP"
],
(parts: string[]) => {
const barrel = parts[1];
const gunType = ExportWeapons[barrel].gunType!;
if (kitgunIsPrimary) {
return {
GT_RIFLE: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary",
GT_SHOTGUN: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryShotgun",
GT_BEAM: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryBeam"
}[gunType];
} else {
return {
GT_RIFLE: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary",
GT_SHOTGUN: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryShotgun",
GT_BEAM: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryBeam"
}[gunType];
}
}
)
];
};
const priceFactor: Record<string, number> = {
Ostron: 0.9,
SolarisUnitedHoverboard: 0.85,
SolarisUnitedMoaPet: 0.95,
SolarisUnitedKitGun: 0.9
};
const getModularWeaponSale = (
partTypeToParts: Record<string, string[]>,
day: number,
name: string,
partTypes: string[],
getItemType: (parts: string[]) => string
): IModularWeaponSaleInfo => {
const rng = new CRng(day);
const parts = partTypes.map(partType => rng.randomElement(partTypeToParts[partType]));
let partsCost = 0;
for (const part of parts) {
const meta = ExportWeapons[part];
if (!meta.premiumPrice) {
throw new Error(`no premium price for ${part}`);
}
partsCost += meta.premiumPrice;
}
return {
Name: name,
Expiry: toMongoDate(new Date((day + 1) * 86400000)),
Revision: day,
Weapons: [
{
ItemType: getItemType(parts),
PremiumPrice: Math.trunc(partsCost * priceFactor[name]),
ModularParts: parts
}
]
};
};
interface IModularWeaponSaleInfo {
Name: string;
Expiry: IMongoDate;
Revision: number;
Weapons: IModularWeaponSaleItem[];
}
interface IModularWeaponSaleItem {
ItemType: string;
PremiumPrice: number;
ModularParts: string[];
}
interface IModularWeaponPurchaseRequest {
SaleName: string;
ItemIndex: number;
Revision: number;
ItemName: string;
PolarizeSlot: number;
PolarizeValue: ArtifactPolarity;
}

View File

@ -8,11 +8,10 @@ interface INameWeaponRequest {
ItemName: string;
}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const nameWeaponController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const body = getJSONfromString(String(req.body)) as INameWeaponRequest;
const body = getJSONfromString<INameWeaponRequest>(String(req.body));
const item = inventory[req.query.Category as string as TEquipmentKey].find(
item => item._id.toString() == (req.query.ItemId as string)
)!;
@ -21,8 +20,9 @@ export const nameWeaponController: RequestHandler = async (req, res) => {
} else {
item.ItemName = undefined;
}
const currencyChanges = updateCurrency(inventory, "webui" in req.query ? 0 : 15, true);
await inventory.save();
res.json({
InventoryChanges: await updateCurrency("webui" in req.query ? 0 : 15, true, accountId)
InventoryChanges: currencyChanges
});
};

View File

@ -0,0 +1,209 @@
import { getInfNodes, getNemesisPasscode } from "@/src/helpers/nemesisHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { freeUpSlot, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { SRng } from "@/src/services/rngService";
import { IMongoDate, IOid } from "@/src/types/commonTypes";
import { IInnateDamageFingerprint, InventorySlot, TEquipmentKey } 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);
if ((req.query.mode as string) == "f") {
const body = getJSONfromString<IValenceFusionRequest>(String(req.body));
const inventory = await getInventory(accountId, 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;
const sourceFingerprint = JSON.parse(sourceWeapon.UpgradeFingerprint!) as IInnateDamageFingerprint;
// Upgrade destination damage type if desireed
if (body.UseSourceDmgType) {
destFingerprint.buffs[0].Tag = sourceFingerprint.buffs[0].Tag;
}
// Upgrade destination damage value
const destDamage = 0.25 + (destFingerprint.buffs[0].Value / 0x3fffffff) * (0.6 - 0.25);
const sourceDamage = 0.25 + (sourceFingerprint.buffs[0].Value / 0x3fffffff) * (0.6 - 0.25);
let newDamage = Math.max(destDamage, sourceDamage) * 1.1;
if (newDamage >= 0.58) {
newDamage = 0.6;
}
destFingerprint.buffs[0].Value = Math.trunc(((newDamage - 0.25) / (0.6 - 0.25)) * 0x3fffffff);
// Commit fingerprint
destWeapon.UpgradeFingerprint = JSON.stringify(destFingerprint);
// Remove source weapon
inventory[body.Category].pull({ _id: body.SourceWeapon.$oid });
freeUpSlot(inventory, InventorySlot.WEAPONS);
await inventory.save();
res.json({
InventoryChanges: {
[body.Category]: [destWeapon.toJSON()]
}
});
} else if ((req.query.mode as string) == "p") {
const inventory = await getInventory(accountId, "Nemesis");
const body = getJSONfromString<INemesisPrespawnCheckRequest>(String(req.body));
const passcode = getNemesisPasscode(inventory.Nemesis!.fp, inventory.Nemesis!.Faction);
let guessResult = 0;
if (inventory.Nemesis!.Faction == "FC_INFESTATION") {
for (let i = 0; i != 3; ++i) {
if (body.guess[i] == passcode[0]) {
guessResult = 1 + i;
break;
}
}
} else {
for (let i = 0; i != 3; ++i) {
if (body.guess[i] == passcode[i]) {
++guessResult;
}
}
}
res.json({ GuessResult: guessResult });
} else if ((req.query.mode as string) == "s") {
const inventory = await getInventory(accountId, "Nemesis");
const body = getJSONfromString<INemesisStartRequest>(String(req.body));
body.target.fp = BigInt(body.target.fp);
let weaponIdx = -1;
if (body.target.Faction != "FC_INFESTATION") {
let weapons: readonly string[];
if (body.target.manifest == "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionSix") {
weapons = kuvaLichVersionSixWeapons;
} else if (
body.target.manifest == "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionFour" ||
body.target.manifest == "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionThree"
) {
weapons = corpusVersionThreeWeapons;
} else {
throw new Error(`unknown nemesis manifest: ${body.target.manifest}`);
}
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);
}
inventory.Nemesis = {
fp: body.target.fp,
manifest: body.target.manifest,
KillingSuit: body.target.KillingSuit,
killingDamageType: body.target.killingDamageType,
ShoulderHelmet: body.target.ShoulderHelmet,
WeaponIdx: weaponIdx,
AgentIdx: body.target.AgentIdx,
BirthNode: body.target.BirthNode,
Faction: body.target.Faction,
Rank: 0,
k: false,
Traded: false,
d: new Date(),
InfNodes: getInfNodes(body.target.Faction, 0),
GuessHistory: [],
Hints: [],
HintProgress: 0,
Weakened: body.target.Weakened,
PrevOwners: 0,
HenchmenKilled: 0,
SecondInCommand: body.target.SecondInCommand,
MissionCount: 0,
LastEnc: 0
};
await inventory.save();
res.json({
target: inventory.toJSON().Nemesis
});
} else {
logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
throw new Error(`unknown nemesis mode: ${String(req.query.mode)}`);
}
};
interface IValenceFusionRequest {
DestWeapon: IOid;
SourceWeapon: IOid;
Category: TEquipmentKey;
UseSourceDmgType: boolean;
}
interface INemesisStartRequest {
target: {
fp: number | bigint;
manifest: string;
KillingSuit: string;
killingDamageType: number;
ShoulderHelmet: string;
DisallowedWeapons: string[];
WeaponIdx: number;
AgentIdx: number;
BirthNode: string;
Faction: string;
Rank: number;
k: boolean;
Traded: boolean;
d: IMongoDate;
InfNodes: [];
GuessHistory: [];
Hints: [];
HintProgress: number;
Weakened: boolean;
PrevOwners: number;
HenchmenKilled: number;
MissionCount?: number; // Added in 38.5.0
LastEnc?: number; // Added in 38.5.0
SecondInCommand: boolean;
};
}
interface INemesisPrespawnCheckRequest {
guess: number[]; // .length == 3
potency?: number[];
}
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"
];

View File

@ -0,0 +1,51 @@
import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, hasGuildPermission } from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
import { RequestHandler } from "express";
import { Types } from "mongoose";
import { ExportDojoRecipes } from "warframe-public-export-plus";
export const placeDecoInComponentController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "GuildId LevelKeys");
const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Decorator))) {
res.json({ DojoRequestStatus: -1 });
return;
}
const request = JSON.parse(String(req.body)) as IPlaceDecoInComponentRequest;
const component = guild.DojoComponents.id(request.ComponentId)!;
if (component.DecoCapacity === undefined) {
component.DecoCapacity = Object.values(ExportDojoRecipes.rooms).find(
x => x.resultType == component.pf
)!.decoCapacity;
}
component.Decos ??= [];
component.Decos.push({
_id: new Types.ObjectId(),
Type: request.Type,
Pos: request.Pos,
Rot: request.Rot,
Name: request.Name
});
const meta = Object.values(ExportDojoRecipes.decos).find(x => x.resultType == request.Type);
if (meta && meta.capacityCost) {
component.DecoCapacity -= meta.capacityCost;
}
await guild.save();
res.json(await getDojoClient(guild, 0, component._id));
};
interface IPlaceDecoInComponentRequest {
ComponentId: string;
Revision: number;
Type: string;
Pos: number[];
Rot: number[];
Name?: string;
}

View File

@ -0,0 +1,31 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IPlayerSkills } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express";
export const playerSkillsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "PlayerSkills");
const request = getJSONfromString<IPlayerSkillsRequest>(String(req.body));
const oldRank: number = inventory.PlayerSkills[request.Skill as keyof IPlayerSkills];
const cost = (request.Pool == "LPP_DRIFTER" ? drifterCosts[oldRank] : 1 << oldRank) * 1000;
inventory.PlayerSkills[request.Pool as keyof IPlayerSkills] -= cost;
inventory.PlayerSkills[request.Skill as keyof IPlayerSkills]++;
await inventory.save();
res.json({
Pool: request.Pool,
PoolInc: -cost,
Skill: request.Skill,
Rank: oldRank + 1
});
};
interface IPlayerSkillsRequest {
Pool: string;
Skill: string;
}
const drifterCosts = [20, 25, 30, 45, 65, 90, 125, 160, 205, 255];

View File

@ -3,7 +3,6 @@ import { getAccountIdForRequest } from "@/src/services/loginService";
import { addMiscItems, getInventory } from "@/src/services/inventoryService";
import { ExportRelics, IRelic } from "warframe-public-export-plus";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const projectionManagerController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
@ -51,6 +50,7 @@ const qualityKeywordToNumber: Record<VoidProjectionQuality, number> = {
// e.g. "/Lotus/Types/Game/Projections/T2VoidProjectionProteaPrimeDBronze" -> ["Lith", "W5", "VPQ_BRONZE"]
const parseProjection = (typeName: string): [string, string, VoidProjectionQuality] => {
const relic: IRelic | undefined = ExportRelics[typeName];
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!relic) {
throw new Error(`Unknown projection ${typeName}`);
}

View File

@ -1,12 +1,14 @@
import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { toPurchaseRequest } from "@/src/helpers/purchaseHelpers";
import { IPurchaseRequest } from "@/src/types/purchaseTypes";
import { handlePurchase } from "@/src/services/purchaseService";
import { getInventory } from "@/src/services/inventoryService";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const purchaseController: RequestHandler = async (req, res) => {
const purchaseRequest = toPurchaseRequest(JSON.parse(String(req.body)));
const purchaseRequest = JSON.parse(String(req.body)) as IPurchaseRequest;
const accountId = await getAccountIdForRequest(req);
const response = await handlePurchase(purchaseRequest, accountId);
const inventory = await getInventory(accountId);
const response = await handlePurchase(purchaseRequest, inventory);
await inventory.save();
res.json(response);
};

View File

@ -1,16 +1,24 @@
import { getGuildForRequest } from "@/src/services/guildService";
import { config } from "@/src/services/configService";
import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, hasGuildPermission } from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
import { RequestHandler } from "express";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const queueDojoComponentDestructionController: RequestHandler = async (req, res) => {
const guild = await getGuildForRequest(req);
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "GuildId LevelKeys");
const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Architect))) {
res.json({ DojoRequestStatus: -1 });
return;
}
const componentId = req.query.componentId as string;
guild.DojoComponents!.splice(
guild.DojoComponents!.findIndex(x => x._id.toString() === componentId),
1
guild.DojoComponents.id(componentId)!.DestructionTime = new Date(
Date.now() + (config.fastDojoRoomDestruction ? 5_000 : 2 * 3600_000)
);
await guild.save();
res.json({
DojoRequestStatus: 1
});
res.json(await getDojoClient(guild, 0, componentId));
};

View File

@ -0,0 +1,34 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { RequestHandler } from "express";
import glyphCodes from "@/static/fixed_responses/glyphsCodes.json";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { addItem, getInventory } from "@/src/services/inventoryService";
export const redeemPromoCodeController: RequestHandler = async (req, res) => {
const body = getJSONfromString<IRedeemPromoCodeRequest>(String(req.body));
if (!(body.codeId in glyphCodes)) {
res.status(400).send("INVALID_CODE").end();
return;
}
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "FlavourItems");
const acquiredGlyphs: string[] = [];
for (const glyph of (glyphCodes as Record<string, string[]>)[body.codeId]) {
if (!inventory.FlavourItems.find(x => x.ItemType == glyph)) {
acquiredGlyphs.push(glyph);
await addItem(inventory, glyph);
}
}
if (acquiredGlyphs.length == 0) {
res.status(400).send("USED_CODE").end();
return;
}
await inventory.save();
res.json({
FlavourItems: acquiredGlyphs
});
};
interface IRedeemPromoCodeRequest {
codeId: string;
}

View File

@ -0,0 +1,70 @@
import { GuildMember } from "@/src/models/guildModel";
import { Account } from "@/src/models/loginModel";
import { getGuildForRequest, hasGuildPermission } from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
import { RequestHandler } from "express";
export const removeFromGuildController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req);
const guild = await getGuildForRequest(req);
const payload = JSON.parse(String(req.body)) as IRemoveFromGuildRequest;
const isKick = !account._id.equals(payload.userId);
if (isKick && !(await hasGuildPermission(guild, account._id, GuildPermission.Regulator))) {
res.status(400).json("Invalid permission");
return;
}
const guildMember = (await GuildMember.findOne({ accountId: payload.userId, guildId: guild._id }))!;
if (guildMember.status == 0) {
const inventory = await getInventory(payload.userId);
inventory.GuildId = undefined;
// Remove clan key or blueprint from kicked member
const itemIndex = inventory.LevelKeys.findIndex(x => x.ItemType == "/Lotus/Types/Keys/DojoKey");
if (itemIndex != -1) {
inventory.LevelKeys.splice(itemIndex, 1);
} else {
const recipeIndex = inventory.Recipes.findIndex(x => x.ItemType == "/Lotus/Types/Keys/DojoKeyBlueprint");
if (recipeIndex != -1) {
inventory.Recipes.splice(itemIndex, 1);
}
}
await inventory.save();
// TODO: Handle clan leader kicking themselves (guild should be deleted in this case, I think)
} else if (guildMember.status == 2) {
// TODO: Maybe the inbox message for the sent invite should be deleted?
}
await GuildMember.deleteOne({ _id: guildMember._id });
guild.RosterActivity ??= [];
if (isKick) {
const kickee = (await Account.findOne({ _id: payload.userId }))!;
guild.RosterActivity.push({
dateTime: new Date(),
entryType: 12,
details: getSuffixedName(kickee) + "," + getSuffixedName(account)
});
} else {
guild.RosterActivity.push({
dateTime: new Date(),
entryType: 7,
details: getSuffixedName(account)
});
}
await guild.save();
res.json({
_id: payload.userId,
ItemToRemove: "/Lotus/Types/Keys/DojoKey",
RecipeToRemove: "/Lotus/Types/Keys/DojoKeyBlueprint"
});
};
interface IRemoveFromGuildRequest {
userId: string;
kicker?: string;
}

View File

@ -1,9 +1,83 @@
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { addMiscItems, getInventory } from "@/src/services/inventoryService";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { createUnveiledRivenFingerprint, randomiseRivenStats, RivenFingerprint } from "@/src/helpers/rivenHelper";
import { ExportUpgrades } from "warframe-public-export-plus";
import { IOid } from "@/src/types/commonTypes";
const rerollRandomModController: RequestHandler = (_req, res) => {
logger.debug("RerollRandomMod Request", { info: _req.body.toString("hex").replace(/(.)(.)/g, "$1$2 ") });
res.json({});
export const rerollRandomModController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const request = getJSONfromString<RerollRandomModRequest>(String(req.body));
if ("ItemIds" in request) {
const inventory = await getInventory(accountId, "Upgrades MiscItems");
const changes: IChange[] = [];
let totalKuvaCost = 0;
request.ItemIds.forEach(itemId => {
const upgrade = inventory.Upgrades.id(itemId)!;
const fingerprint = JSON.parse(upgrade.UpgradeFingerprint!) as RivenFingerprint;
if ("challenge" in fingerprint) {
upgrade.UpgradeFingerprint = JSON.stringify(
createUnveiledRivenFingerprint(ExportUpgrades[upgrade.ItemType])
);
} else {
fingerprint.rerolls ??= 0;
const kuvaCost = fingerprint.rerolls < rerollCosts.length ? rerollCosts[fingerprint.rerolls] : 3500;
totalKuvaCost += kuvaCost;
addMiscItems(inventory, [
{
ItemType: "/Lotus/Types/Items/MiscItems/Kuva",
ItemCount: kuvaCost * -1
}
]);
fingerprint.rerolls++;
upgrade.UpgradeFingerprint = JSON.stringify(fingerprint);
randomiseRivenStats(ExportUpgrades[upgrade.ItemType], fingerprint);
upgrade.PendingRerollFingerprint = JSON.stringify(fingerprint);
}
changes.push({
ItemId: { $oid: request.ItemIds[0] },
UpgradeFingerprint: upgrade.UpgradeFingerprint,
PendingRerollFingerprint: upgrade.PendingRerollFingerprint
});
});
await inventory.save();
res.json({
changes: changes,
cost: totalKuvaCost
});
} else {
const inventory = await getInventory(accountId, "Upgrades");
const upgrade = inventory.Upgrades.id(request.ItemId)!;
if (request.CommitReroll && upgrade.PendingRerollFingerprint) {
upgrade.UpgradeFingerprint = upgrade.PendingRerollFingerprint;
}
upgrade.PendingRerollFingerprint = undefined;
await inventory.save();
res.send(upgrade.UpgradeFingerprint);
}
};
export { rerollRandomModController };
type RerollRandomModRequest = LetsGoGamblingRequest | AwDangitRequest;
interface LetsGoGamblingRequest {
ItemIds: string[];
}
interface AwDangitRequest {
ItemId: string;
CommitReroll: boolean;
}
interface IChange {
ItemId: IOid;
UpgradeFingerprint?: string;
PendingRerollFingerprint?: string;
}
const rerollCosts = [900, 1000, 1200, 1400, 1700, 2000, 2350, 2750, 3150];

View File

@ -0,0 +1,91 @@
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { ICompletedDialogue } from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express";
export const saveDialogueController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const request = JSON.parse(String(req.body)) as SaveDialogueRequest;
if ("YearIteration" in request) {
const inventory = await getInventory(accountId);
if (inventory.DialogueHistory) {
inventory.DialogueHistory.YearIteration = request.YearIteration;
} else {
inventory.DialogueHistory = { YearIteration: request.YearIteration };
}
await inventory.save();
res.end();
} else {
const inventory = await getInventory(accountId);
if (!inventory.DialogueHistory) {
throw new Error("bad inventory state");
}
if (request.QueuedDialogues.length != 0 || request.OtherDialogueInfos.length != 0) {
logger.error(`saveDialogue request not fully handled: ${String(req.body)}`);
}
inventory.DialogueHistory.Dialogues ??= [];
let dialogue = inventory.DialogueHistory.Dialogues.find(x => x.DialogueName == request.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),
Gifts: [],
Booleans: [],
Completed: [],
DialogueName: request.DialogueName
}) - 1
];
}
dialogue.Rank = request.Rank;
dialogue.Chemistry = request.Chemistry;
//dialogue.QueuedDialogues = request.QueuedDialogues;
for (const bool of request.Booleans) {
dialogue.Booleans.push(bool);
}
for (const bool of request.ResetBooleans) {
const index = dialogue.Booleans.findIndex(x => x == bool);
if (index != -1) {
dialogue.Booleans.splice(index, 1);
}
}
dialogue.Completed.push(request.Data);
const tomorrowAt0Utc = (Math.trunc(Date.now() / (86400 * 1000)) + 1) * 86400 * 1000;
dialogue.AvailableDate = new Date(tomorrowAt0Utc);
await inventory.save();
res.json({
InventoryChanges: [],
AvailableDate: { $date: { $numberLong: tomorrowAt0Utc.toString() } }
});
}
};
type SaveDialogueRequest = SaveYearIterationRequest | SaveCompletedDialogueRequest;
interface SaveYearIterationRequest {
YearIteration: number;
}
interface SaveCompletedDialogueRequest {
DialogueName: string;
Rank: number;
Chemistry: number;
CompletionType: number;
QueuedDialogues: string[]; // unsure
Booleans: string[];
ResetBooleans: string[];
Data: ICompletedDialogue;
OtherDialogueInfos: IOtherDialogueInfo[]; // unsure
}
interface IOtherDialogueInfo {
Dialogue: string;
Tag: string;
Value: number;
}

View File

@ -4,7 +4,6 @@ import { handleInventoryItemConfigChange } from "@/src/services/saveLoadoutServi
import { getAccountIdForRequest } from "@/src/services/loginService";
import { logger } from "@/src/utils/logger";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const saveLoadoutController: RequestHandler = async (req, res) => {
//validate here
const accountId = await getAccountIdForRequest(req);

View File

@ -0,0 +1,22 @@
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory } from "@/src/services/inventoryService";
import { RequestHandler } from "express";
import { ISettings } from "../../types/inventoryTypes/inventoryTypes";
interface ISaveSettingsRequest {
Settings: ISettings;
}
const saveSettingsController: RequestHandler = async (req, res): Promise<void> => {
const accountId = await getAccountIdForRequest(req);
const settingResults = getJSONfromString<ISaveSettingsRequest>(String(req.body));
const inventory = await getInventory(accountId);
inventory.Settings = Object.assign(inventory.Settings, settingResults.Settings);
await inventory.save();
res.json(inventory.Settings);
};
export { saveSettingsController };

View File

@ -1,9 +1,15 @@
import { RequestHandler } from "express";
import { ISellRequest } from "@/src/types/sellTypes";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory, addMods, addRecipes } from "@/src/services/inventoryService";
import {
getInventory,
addMods,
addRecipes,
addMiscItems,
addConsumables,
freeUpSlot
} from "@/src/services/inventoryService";
import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const sellController: RequestHandler = async (req, res) => {
const payload = JSON.parse(String(req.body)) as ISellRequest;
const accountId = await getAccountIdForRequest(req);
@ -14,6 +20,20 @@ export const sellController: RequestHandler = async (req, res) => {
inventory.RegularCredits += payload.SellPrice;
} else if (payload.SellCurrency == "SC_FusionPoints") {
inventory.FusionPoints += payload.SellPrice;
} else if (payload.SellCurrency == "SC_PrimeBucks") {
addMiscItems(inventory, [
{
ItemType: "/Lotus/Types/Items/MiscItems/PrimeBucks",
ItemCount: payload.SellPrice
}
]);
} else if (payload.SellCurrency == "SC_DistillPoints") {
addMiscItems(inventory, [
{
ItemType: "/Lotus/Types/Items/MiscItems/DistillPoints",
ItemCount: payload.SellPrice
}
]);
} else {
throw new Error("Unknown SellCurrency: " + payload.SellCurrency);
}
@ -22,23 +42,84 @@ export const sellController: RequestHandler = async (req, res) => {
if (payload.Items.Suits) {
payload.Items.Suits.forEach(sellItem => {
inventory.Suits.pull({ _id: sellItem.String });
freeUpSlot(inventory, InventorySlot.SUITS);
});
}
if (payload.Items.LongGuns) {
payload.Items.LongGuns.forEach(sellItem => {
inventory.LongGuns.pull({ _id: sellItem.String });
freeUpSlot(inventory, InventorySlot.WEAPONS);
});
}
if (payload.Items.Pistols) {
payload.Items.Pistols.forEach(sellItem => {
inventory.Pistols.pull({ _id: sellItem.String });
freeUpSlot(inventory, InventorySlot.WEAPONS);
});
}
if (payload.Items.Melee) {
payload.Items.Melee.forEach(sellItem => {
inventory.Melee.pull({ _id: sellItem.String });
freeUpSlot(inventory, InventorySlot.WEAPONS);
});
}
if (payload.Items.SpaceSuits) {
payload.Items.SpaceSuits.forEach(sellItem => {
inventory.SpaceSuits.pull({ _id: sellItem.String });
freeUpSlot(inventory, InventorySlot.SPACESUITS);
});
}
if (payload.Items.SpaceGuns) {
payload.Items.SpaceGuns.forEach(sellItem => {
inventory.SpaceGuns.pull({ _id: sellItem.String });
freeUpSlot(inventory, InventorySlot.SPACEWEAPONS);
});
}
if (payload.Items.SpaceMelee) {
payload.Items.SpaceMelee.forEach(sellItem => {
inventory.SpaceMelee.pull({ _id: sellItem.String });
freeUpSlot(inventory, InventorySlot.SPACEWEAPONS);
});
}
if (payload.Items.Sentinels) {
payload.Items.Sentinels.forEach(sellItem => {
inventory.Sentinels.pull({ _id: sellItem.String });
freeUpSlot(inventory, InventorySlot.SENTINELS);
});
}
if (payload.Items.SentinelWeapons) {
payload.Items.SentinelWeapons.forEach(sellItem => {
inventory.SentinelWeapons.pull({ _id: sellItem.String });
freeUpSlot(inventory, InventorySlot.SENTINELS);
});
}
if (payload.Items.OperatorAmps) {
payload.Items.OperatorAmps.forEach(sellItem => {
inventory.OperatorAmps.pull({ _id: sellItem.String });
freeUpSlot(inventory, InventorySlot.AMPS);
});
}
if (payload.Items.Hoverboards) {
payload.Items.Hoverboards.forEach(sellItem => {
inventory.Hoverboards.pull({ _id: sellItem.String });
freeUpSlot(inventory, InventorySlot.SPACESUITS);
});
}
if (payload.Items.Drones) {
payload.Items.Drones.forEach(sellItem => {
inventory.Drones.pull({ _id: sellItem.String });
});
}
if (payload.Items.Consumables) {
const consumablesChanges = [];
for (const sellItem of payload.Items.Consumables) {
consumablesChanges.push({
ItemType: sellItem.String,
ItemCount: sellItem.Count * -1
});
}
addConsumables(inventory, consumablesChanges);
}
if (payload.Items.Recipes) {
const recipeChanges = [];
for (const sellItem of payload.Items.Recipes) {
@ -63,7 +144,52 @@ export const sellController: RequestHandler = async (req, res) => {
}
});
}
if (payload.Items.MiscItems) {
payload.Items.MiscItems.forEach(sellItem => {
addMiscItems(inventory, [
{
ItemType: sellItem.String,
ItemCount: sellItem.Count * -1
}
]);
});
}
await inventory.save();
res.json({});
};
interface ISellRequest {
Items: {
Suits?: ISellItem[];
LongGuns?: ISellItem[];
Pistols?: ISellItem[];
Melee?: ISellItem[];
Consumables?: ISellItem[];
Recipes?: ISellItem[];
Upgrades?: ISellItem[];
MiscItems?: ISellItem[];
SpaceSuits?: ISellItem[];
SpaceGuns?: ISellItem[];
SpaceMelee?: ISellItem[];
Sentinels?: ISellItem[];
SentinelWeapons?: ISellItem[];
OperatorAmps?: ISellItem[];
Hoverboards?: ISellItem[];
Drones?: ISellItem[];
};
SellPrice: number;
SellCurrency:
| "SC_RegularCredits"
| "SC_PrimeBucks"
| "SC_FusionPoints"
| "SC_DistillPoints"
| "SC_CrewShipFusionPoints"
| "SC_Resources";
buildLabel: string;
}
interface ISellItem {
String: string; // oid or uniqueName
Count: number;
}

View File

@ -1,7 +1,18 @@
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
const setActiveQuestController: RequestHandler = (_req, res) => {
res.sendStatus(200);
};
export const setActiveQuestController: RequestHandler<
Record<string, never>,
undefined,
undefined,
{ quest: string | undefined }
> = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const quest = req.query.quest;
export { setActiveQuestController };
const inventory = await getInventory(accountId, "ActiveQuest");
inventory.ActiveQuest = quest ?? "";
await inventory.save();
res.status(200).end();
};

View File

@ -4,7 +4,6 @@ import { parseString } from "@/src/helpers/general";
import { RequestHandler } from "express";
import { Types } from "mongoose";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const setActiveShipController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const shipId = parseString(req.query.shipId);

View File

@ -2,12 +2,23 @@ import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getPersonalRooms } from "@/src/services/personalRoomsService";
import { TBootLocation } from "@/src/types/shipTypes";
import { getInventory } from "@/src/services/inventoryService";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const setBootLocationController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const personalRooms = await getPersonalRooms(accountId);
personalRooms.Ship.BootLocation = req.query.bootLocation as string as TBootLocation;
await personalRooms.save();
if (personalRooms.Ship.BootLocation == "SHOP") {
// Temp fix so the motorcycle in the backroom doesn't appear broken.
// This code may be removed when quests are fully implemented.
const inventory = await getInventory(accountId);
if (inventory.Motorcycles.length == 0) {
inventory.Motorcycles.push({ ItemType: "/Lotus/Types/Vehicles/Motorcycle/MotorcyclePowerSuit" });
await inventory.save();
}
}
res.end();
};

View File

@ -0,0 +1,18 @@
import { RequestHandler } from "express";
import { getDojoClient, getGuildForRequest } from "@/src/services/guildService";
export const setDojoComponentMessageController: RequestHandler = async (req, res) => {
const guild = await getGuildForRequest(req);
// At this point, we know that a member of the guild is making this request. Assuming they are allowed to change the message.
const component = guild.DojoComponents.id(req.query.componentId as string)!;
const payload = JSON.parse(String(req.body)) as SetDojoComponentMessageRequest;
if ("Name" in payload) {
component.Name = payload.Name;
} else {
component.Message = payload.Message;
}
await guild.save();
res.json(await getDojoClient(guild, 0, component._id));
};
type SetDojoComponentMessageRequest = { Name: string } | { Message: string };

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