Compare commits

...

418 Commits

Author SHA1 Message Date
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
254 changed files with 27649 additions and 22233 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": { "rules": {
"@typescript-eslint/explicit-function-return-type": "warn", "@typescript-eslint/explicit-function-return-type": "warn",
"@typescript-eslint/explicit-module-boundary-types": "warn",
"@typescript-eslint/restrict-template-expressions": "warn", "@typescript-eslint/restrict-template-expressions": "warn",
"@typescript-eslint/restrict-plus-operands": "warn", "@typescript-eslint/restrict-plus-operands": "warn",
"@typescript-eslint/no-unsafe-member-access": "warn", "@typescript-eslint/no-unsafe-member-access": "warn",
@ -23,10 +22,13 @@
"@typescript-eslint/no-unsafe-assignment": "warn", "@typescript-eslint/no-unsafe-assignment": "warn",
"@typescript-eslint/no-explicit-any": "warn", "@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-loss-of-precision": "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", "prettier/prettier": "error",
"@typescript-eslint/semi": "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", "parser": "@typescript-eslint/parser",
"parserOptions": { "parserOptions": {

View File

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

3
.gitignore vendored
View File

@ -16,3 +16,6 @@ yarn.lock
# MongoDB VSCode extension playground scripts # MongoDB VSCode extension playground scripts
/database_scripts /database_scripts
# Default Docker directory
/docker-data

View File

@ -1,2 +1,3 @@
static/webui/libs/ static/webui/libs/
*.html *.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,11 @@
# Space Ninja Server # Space Ninja Server
More information for the moment here: [https://discord.gg/PNNZ3asUuY](https://discord.gg/PNNZ3asUuY) More information for the moment here: [https://discord.gg/PNNZ3asUuY](https://discord.gg/PNNZ3asUuY)
>[!NOTE]
>Development of this project currently happens on <https://onlyg.it/OpenWF/SpaceNinjaServer>. If that's not the site you're on, you're looking at a one-way mirror.
## 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,25 @@
@echo off
echo Updating SpaceNinjaServer...
git config remote.origin.url https://openwf.io/SpaceNinjaServer.git
git fetch --prune
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
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,39 @@
"mongodbUrl": "mongodb://127.0.0.1:27017/openWF", "mongodbUrl": "mongodb://127.0.0.1:27017/openWF",
"logger": { "logger": {
"files": true, "files": true,
"level": "trace", "level": "trace"
"__valid_levels": "fatal, error, warn, info, http, debug, trace"
}, },
"myAddress": "localhost", "myAddress": "localhost",
"hubAddress": "https://localhost/api/",
"platformCDNs": ["https://localhost/"],
"NRS": ["localhost"],
"httpPort": 80, "httpPort": 80,
"httpsPort": 443, "httpsPort": 443,
"administratorNames": [],
"autoCreateAccount": true, "autoCreateAccount": true,
"skipStoryModeChoice": true,
"skipTutorial": true, "skipTutorial": true,
"skipAllDialogue": true, "skipAllDialogue": true,
"unlockAllScans": true, "unlockAllScans": true,
"unlockAllMissions": true, "unlockAllMissions": true,
"unlockAllQuests": true, "infiniteCredits": true,
"completeAllQuests": false, "infinitePlatinum": true,
"infiniteResources": true, "infiniteEndo": true,
"infiniteRegalAya": true,
"infiniteHelminthMaterials": false,
"unlockAllShipFeatures": true, "unlockAllShipFeatures": true,
"unlockAllShipDecorations": true, "unlockAllShipDecorations": true,
"unlockAllFlavourItems": true, "unlockAllFlavourItems": true,
"unlockAllSkins": true, "unlockAllSkins": true,
"unlockAllCapturaScenes": true,
"universalPolarityEverywhere": true, "universalPolarityEverywhere": true,
"unlockDoubleCapacityPotatoesEverywhere": true,
"unlockExilusEverywhere": true,
"unlockArcanesEverywhere": true,
"noDailyStandingLimits": true,
"instantResourceExtractorDrones": false,
"noDojoRoomBuildStage": true,
"fastDojoRoomDestruction": true,
"noDojoResearchCosts": true,
"noDojoResearchTime": true,
"spoofMasteryRank": -1 "spoofMasteryRank": -1
} }

View File

@ -1,24 +1,43 @@
version: "3.9"
services: services:
mongodb: spaceninjaserver:
container_name: mongodb # build: .
image: mongodb image: openwf/spaceninjaserver:latest
restart: always
build:
context: .
dockerfile: Dockerfile
target: base
environment: environment:
MONGO_INITDB_ROOT_USERNAME: ${DATABASE_USERNAME} APP_MONGODB_URL: mongodb://openwfagent:spaceninjaserver@mongodb:27017/
MONGO_INITDB_ROOT_PASSWORD: ${DATABASE_PASSWORD}
ports:
- ${DATABASE_PORT}:${DATABASE_PORT}
expose:
- "${DATABASE_PORT}"
networks:
- docker
networks: # Following environment variables are set to default image values.
docker: # Uncomment to edit.
external: true
# 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

2162
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,30 +9,32 @@
"build": "tsc && copyfiles static/webui/** build", "build": "tsc && copyfiles static/webui/** build",
"lint": "eslint --ext .ts .", "lint": "eslint --ext .ts .",
"lint:fix": "eslint --fix --ext .ts .", "lint:fix": "eslint --fix --ext .ts .",
"prettier": "prettier --write ." "prettier": "prettier --write .",
"update-translations": "cd scripts && node update-translations.js"
}, },
"license": "GNU", "license": "GNU",
"dependencies": { "dependencies": {
"copyfiles": "^2.4.1", "copyfiles": "^2.4.1",
"express": "^5.0.0-beta.3", "express": "^5",
"mongoose": "^8.4.5", "mongoose": "^8.11.0",
"warframe-public-export-plus": "^0.4.4", "warframe-public-export-plus": "^0.5.39",
"warframe-riven-info": "^0.1.1", "warframe-riven-info": "^0.1.2",
"winston": "^3.13.0", "winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0" "winston-daily-rotate-file": "^5.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/express": "^4.17.20", "@types/express": "^5",
"@types/morgan": "^1.9.9", "@types/morgan": "^1.9.9",
"@typescript-eslint/eslint-plugin": "^7.14", "@typescript-eslint/eslint-plugin": "^7.18",
"@typescript-eslint/parser": "^7.14", "@typescript-eslint/parser": "^7.18",
"eslint": "^8.56.0", "eslint": "^8.56.0",
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-prettier": "^5.2.3",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"prettier": "^3.3.2", "prettier": "^3.4.2",
"ts-node": "^10.9.2",
"ts-node-dev": "^2.0.0", "ts-node-dev": "^2.0.0",
"tsconfig-paths": "^4.2.0", "tsconfig-paths": "^4.2.0",
"typescript": "^5.5" "typescript": ">=4.7.4 <5.6.0"
}, },
"engines": { "engines": {
"node": ">=18.15.0", "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,34 @@
import express from "express"; import express from "express";
import bodyParser from "body-parser";
import { unknownEndpointHandler } from "@/src/middleware/middleware"; import { unknownEndpointHandler } from "@/src/middleware/middleware";
import { requestLogger } from "@/src/middleware/morgenMiddleware"; import { requestLogger } from "@/src/middleware/morgenMiddleware";
import { errorHandler } from "@/src/middleware/errorHandler";
import { apiRouter } from "@/src/routes/api"; import { apiRouter } from "@/src/routes/api";
//import { testRouter } from "@/src/routes/test";
import { cacheRouter } from "@/src/routes/cache"; 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 { customRouter } from "@/src/routes/custom";
import { dynamicController } from "@/src/routes/dynamic"; import { dynamicController } from "@/src/routes/dynamic";
import { payRouter } from "@/src/routes/pay";
import { statsRouter } from "@/src/routes/stats"; import { statsRouter } from "@/src/routes/stats";
import { webuiRouter } from "@/src/routes/webui"; 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(); const app = express();
app.use(bodyParser.raw()); app.use(bodyParser.raw());
app.use(express.json()); app.use(express.json({ limit: "4mb" }));
app.use(bodyParser.text()); app.use(bodyParser.text());
app.use(requestLogger); app.use(requestLogger);
//app.use(requestLogger);
app.use("/api", apiRouter); app.use("/api", apiRouter);
//app.use("/test", testRouter);
app.use("/", cacheRouter); app.use("/", cacheRouter);
app.use("/custom", customRouter); app.use("/custom", customRouter);
app.use("/:id/dynamic", dynamicController); app.use("/:id/dynamic", dynamicController);
app.use("/pay", payRouter);
app.post("/pay/steamPacks.php", steamPacksController);
app.use("/stats", statsRouter); app.use("/stats", statsRouter);
app.use("/", webuiRouter); app.use("/", webuiRouter);
app.use(unknownEndpointHandler); app.use(unknownEndpointHandler);
app.use(errorHandler);
//app.use(errorHandler)
export { app }; 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,26 @@
import { getDojoClient, getGuildForRequestEx, removeDojoDeco, removeDojoRoom } from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const abortDojoComponentController: 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 IAbortDojoComponentRequest;
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,12 @@
import { getDojoClient, getGuildForRequest } from "@/src/services/guildService";
import { RequestHandler } from "express";
export const abortDojoComponentDestructionController: RequestHandler = async (req, res) => {
const guild = await getGuildForRequest(req);
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,97 @@
import { toOid } from "@/src/helpers/inventoryHelpers";
import { IRivenChallenge } from "@/src/helpers/rivenFingerprintHelper";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { addMods, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getRandomElement, getRandomInt, getRandomReward } from "@/src/services/rngService";
import { logger } from "@/src/utils/logger";
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 challenge = getRandomElement(ExportUpgrades[rivenType].availableChallenges!);
const fingerprintChallenge: IRivenChallenge = {
Type: challenge.fullName,
Progress: 0,
Required: getRandomInt(challenge.countRange[0], challenge.countRange[1])
};
if (Math.random() < challenge.complicationChance) {
const complications: { type: string; probability: number }[] = [];
for (const complication of challenge.complications) {
complications.push({
type: complication.fullName,
probability: complication.weight
});
}
fingerprintChallenge.Complication = getRandomReward(complications)!.type;
logger.debug(
`riven rolled challenge ${fingerprintChallenge.Type} with complication ${fingerprintChallenge.Complication}`
);
const complication = challenge.complications.find(x => x.fullName == fingerprintChallenge.Complication)!;
fingerprintChallenge.Required *= complication.countMultiplier;
} else {
logger.debug(`riven rolled challenge ${fingerprintChallenge.Type}`);
}
const upgradeIndex =
inventory.Upgrades.push({
ItemType: rivenType,
UpgradeFingerprint: JSON.stringify({ challenge: fingerprintChallenge })
}) - 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: { challenge: fingerprintChallenge },
ItemType: inventory.Upgrades[upgradeIndex].ItemType,
ItemId: toOid(inventory.Upgrades[upgradeIndex]._id)
}
});
};
interface IActiveRandomModRequest {
ItemType: string;
}
const rivenRawToRealWeighted: Record<string, string[]> = {
"/Lotus/Upgrades/Mods/Randomized/RawArchgunRandomMod": [
"/Lotus/Upgrades/Mods/Randomized/LotusArchgunRandomModRare"
],
"/Lotus/Upgrades/Mods/Randomized/RawMeleeRandomMod": [
"/Lotus/Upgrades/Mods/Randomized/PlayerMeleeWeaponRandomModRare"
],
"/Lotus/Upgrades/Mods/Randomized/RawModularMeleeRandomMod": [
"/Lotus/Upgrades/Mods/Randomized/LotusModularMeleeRandomModRare"
],
"/Lotus/Upgrades/Mods/Randomized/RawModularPistolRandomMod": [
"/Lotus/Upgrades/Mods/Randomized/LotusModularPistolRandomModRare"
],
"/Lotus/Upgrades/Mods/Randomized/RawPistolRandomMod": ["/Lotus/Upgrades/Mods/Randomized/LotusPistolRandomModRare"],
"/Lotus/Upgrades/Mods/Randomized/RawRifleRandomMod": ["/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare"],
"/Lotus/Upgrades/Mods/Randomized/RawShotgunRandomMod": [
"/Lotus/Upgrades/Mods/Randomized/LotusShotgunRandomModRare"
],
"/Lotus/Upgrades/Mods/Randomized/RawSentinelWeaponRandomMod": [
"/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare",
"/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare",
"/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare",
"/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare",
"/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare",
"/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare",
"/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare",
"/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare",
"/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare",
"/Lotus/Upgrades/Mods/Randomized/LotusShotgunRandomModRare",
"/Lotus/Upgrades/Mods/Randomized/LotusPistolRandomModRare",
"/Lotus/Upgrades/Mods/Randomized/PlayerMeleeWeaponRandomModRare"
]
};

View File

@ -4,10 +4,9 @@ import { IUpdateGlyphRequest } from "@/src/types/requestTypes";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
const addFriendImageController: RequestHandler = async (req, res) => { const addFriendImageController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const json = getJSONfromString(String(req.body)) as IUpdateGlyphRequest; const json = getJSONfromString<IUpdateGlyphRequest>(String(req.body));
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
inventory.ActiveAvatarImageType = json.AvatarImageType; inventory.ActiveAvatarImageType = json.AvatarImageType;
await inventory.save(); await inventory.save();

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

@ -1,22 +1,70 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { upgradeMod } from "@/src/services/inventoryService";
import { IArtifactsRequest } from "@/src/types/requestTypes";
import { RequestHandler } from "express"; 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 export const artifactsController: RequestHandler = async (req, res) => {
const artifactsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const artifactsData = getJSONfromString<IArtifactsRequest>(String(req.body));
try { const { Upgrade, LevelDiff, Cost, FusionPointCost } = artifactsData;
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call
const artifactsData = getJSONfromString(req.body.toString()) as IArtifactsRequest; const inventory = await getInventory(accountId);
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument const { Upgrades } = inventory;
const upgradeModId = await upgradeMod(artifactsData, accountId); const { ItemType, UpgradeFingerprint, ItemId } = Upgrade;
res.send(upgradeModId);
} catch (err) { const safeUpgradeFingerprint = UpgradeFingerprint || '{"lvl":0}';
console.error("Error parsing JSON data:", err); 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,90 @@
import { RequestHandler } from "express";
import { getDojoClient, getGuildForRequest } from "@/src/services/guildService";
import { logger } from "@/src/utils/logger";
import { IDojoComponentDatabase } from "@/src/types/guildTypes";
import { Types } from "mongoose";
export const changeDojoRootController: 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 root.
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

@ -8,55 +8,91 @@ import { IOid } from "@/src/types/commonTypes";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory, updateCurrency, addItem, addMiscItems, addRecipes } from "@/src/services/inventoryService"; import { getInventory, updateCurrency, addItem, addMiscItems, addRecipes } from "@/src/services/inventoryService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
export interface IClaimCompletedRecipeRequest { export interface IClaimCompletedRecipeRequest {
RecipeIds: IOid[]; RecipeIds: IOid[];
} }
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const claimCompletedRecipeController: RequestHandler = async (req, res) => { 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); const accountId = await getAccountIdForRequest(req);
if (!accountId) throw new Error("no account id"); if (!accountId) throw new Error("no account id");
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const pendingRecipe = inventory.PendingRecipes.find( const pendingRecipe = inventory.PendingRecipes.id(claimCompletedRecipeRequest.RecipeIds[0].$oid);
recipe => recipe._id?.toString() === claimCompletedRecipeRequest.RecipeIds[0].$oid
);
if (!pendingRecipe) { 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}`); throw new Error(`no pending recipe found with id ${claimCompletedRecipeRequest.RecipeIds[0].$oid}`);
} }
//check recipe is indeed ready to be completed //check recipe is indeed ready to be completed
// if (pendingRecipe.CompletionDate > new Date()) { // 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`); // throw new Error(`recipe ${pendingRecipe._id} is not ready to be completed`);
// } // }
inventory.PendingRecipes.pull(pendingRecipe._id); inventory.PendingRecipes.pull(pendingRecipe._id);
await inventory.save();
const recipe = getRecipe(pendingRecipe.ItemType); const recipe = getRecipe(pendingRecipe.ItemType);
if (!recipe) { if (!recipe) {
logger.error(`no completed item found for recipe ${pendingRecipe._id}`); throw new Error(`no completed item found for recipe ${pendingRecipe._id.toString()}`);
throw new Error(`no completed item found for recipe ${pendingRecipe._id}`);
} }
if (req.query.cancel) { 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); const nonMiscItemIngredients = new Set();
addMiscItems(inventory, recipe.ingredients); for (const category of ["LongGuns", "Pistols", "Melee"] as const) {
await inventory.save(); 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. inventoryChanges.WeaponBin ??= { Slots: 0 };
res.json({ inventoryChanges.WeaponBin.Slots -= 1;
...currencyChanges, });
MiscItems: recipe.ingredients }
}
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 { } else {
logger.debug("Claiming Recipe", { recipe, pendingRecipe }); 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 = {}; let InventoryChanges = {};
if (recipe.consumeOnUse) { if (recipe.consumeOnUse) {
const recipeChanges = [ const recipeChanges = [
@ -68,21 +104,19 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
InventoryChanges = { ...InventoryChanges, Recipes: recipeChanges }; InventoryChanges = { ...InventoryChanges, Recipes: recipeChanges };
const inventory = await getInventory(accountId);
addRecipes(inventory, recipeChanges); addRecipes(inventory, recipeChanges);
await inventory.save();
} }
if (req.query.rush) { if (req.query.rush) {
InventoryChanges = { InventoryChanges = {
...InventoryChanges, ...InventoryChanges,
...(await updateCurrency(recipe.skipBuildTimePrice, true, accountId)) ...updateCurrency(inventory, recipe.skipBuildTimePrice, true)
}; };
} }
res.json({ InventoryChanges = {
InventoryChanges: { ...InventoryChanges,
...InventoryChanges, ...(await addItem(inventory, recipe.resultType, recipe.num, false)).InventoryChanges
...(await addItem(accountId, recipe.resultType, recipe.num)).InventoryChanges };
} 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/rivenFingerprintHelper";
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,118 @@
import { TGuildDatabaseDocument } from "@/src/models/guildModel";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { getDojoClient, getGuildForRequestEx, scaleRequiredCount } 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, IDojoRecipe } from "warframe-public-export-plus";
interface IContributeToDojoComponentRequest {
ComponentId: string;
DecoId?: string;
DecoType?: string;
IngredientContributions: {
ItemType: string;
ItemCount: number;
}[];
RegularCredits: number;
VaultIngredientContributions: [];
VaultCredits: number;
}
export const contributeToDojoComponentController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const guild = await getGuildForRequestEx(req, inventory);
// 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.
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)!;
await processContribution(guild, request, inventory, inventoryChanges, meta, 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)!;
await 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 = async (
guild: TGuildDatabaseDocument,
request: IContributeToDojoComponentRequest,
inventory: TInventoryDatabaseDocument,
inventoryChanges: IInventoryChanges,
meta: IDojoRecipe,
component: IDojoContributable
): Promise<void> => {
component.RegularCredits ??= 0;
if (component.RegularCredits + request.RegularCredits > scaleRequiredCount(meta.price)) {
request.RegularCredits = scaleRequiredCount(meta.price) - component.RegularCredits;
}
component.RegularCredits += request.RegularCredits;
inventoryChanges.RegularCredits = -request.RegularCredits;
updateCurrency(inventory, request.RegularCredits, false);
component.MiscItems ??= [];
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) {
if (request.IngredientContributions.length) {
// We've already updated subpaths of MiscItems, we need to allow MongoDB to save this before we remove MiscItems.
await guild.save();
}
component.RegularCredits = undefined;
component.MiscItems = undefined;
component.CompletionTime = new Date(Date.now() + meta.time * 1000);
}
}
};

View File

@ -3,12 +3,10 @@ import { getAccountIdForRequest } from "@/src/services/loginService";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Inventory } from "@/src/models/inventoryModels/inventoryModel"; import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
import { Guild } from "@/src/models/guildModel"; import { Guild } from "@/src/models/guildModel";
import { ICreateGuildRequest } from "@/src/types/guildTypes";
// eslint-disable-next-line @typescript-eslint/no-misused-promises export const createGuildController: RequestHandler = async (req, res) => {
const createGuildController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const payload = getJSONfromString(String(req.body)) as ICreateGuildRequest; const payload = getJSONfromString<ICreateGuildRequest>(String(req.body));
// Create guild on database // Create guild on database
const guild = new Guild({ const guild = new Guild({
@ -23,7 +21,6 @@ const createGuildController: RequestHandler = async (req, res) => {
inventory.GuildId = guild._id; inventory.GuildId = guild._id;
// Give clan key (TODO: This should only be a blueprint) // Give clan key (TODO: This should only be a blueprint)
inventory.LevelKeys ??= [];
inventory.LevelKeys.push({ inventory.LevelKeys.push({
ItemType: "/Lotus/Types/Keys/DojoKey", ItemType: "/Lotus/Types/Keys/DojoKey",
ItemCount: 1 ItemCount: 1
@ -35,4 +32,6 @@ const createGuildController: RequestHandler = async (req, res) => {
res.json(guild); res.json(guild);
}; };
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 = 999999999;
response.PremiumCredits = 999999999;
}
res.json(response);
};

View File

@ -0,0 +1,18 @@
import { getDojoClient, getGuildForRequest, removeDojoDeco } from "@/src/services/guildService";
import { RequestHandler } from "express";
export const destroyDojoDecoController: RequestHandler = async (req, res) => {
const guild = await getGuildForRequest(req);
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,50 @@
import { getDojoClient, getGuildForRequestEx, 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, IDojoRecipe } 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);
const guild = await getGuildForRequestEx(req, inventory);
const request = JSON.parse(String(req.body)) as IDojoComponentRushRequest;
const component = guild.DojoComponents.id(request.ComponentId)!;
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, request.Amount);
} else {
const meta = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == component.pf)!;
processContribution(component, meta, request.Amount);
}
const inventoryChanges = updateCurrency(inventory, request.Amount, true);
await guild.save();
await inventory.save();
res.json({
...(await getDojoClient(guild, 0, component._id)),
InventoryChanges: inventoryChanges
});
};
const processContribution = (component: IDojoContributable, meta: IDojoRecipe, 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
);
};

View File

@ -1,7 +1,140 @@
import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers";
import { config } from "@/src/services/configService";
import { addMiscItems, getInventory } from "@/src/services/inventoryService";
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 { RequestHandler } from "express";
import { ExportDrones, ExportResources, ExportSystems } from "warframe-public-export-plus";
const dronesController: RequestHandler = (_req, res) => { export const dronesController: RequestHandler = async (req, res) => {
res.json({}); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
if ("GetActive" in req.query) {
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 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 = "/Lotus/" + resource.StoreItem.substring(18);
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 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

@ -1,32 +1,48 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService"; 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 { 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"; import { EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const evolveWeaponController: RequestHandler = async (req, res) => { export const evolveWeaponController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const payload = getJSONfromString(String(req.body)) as IEvolveWeaponRequest; const payload = getJSONfromString<IEvolveWeaponRequest>(String(req.body));
console.assert(payload.Action == "EWA_INSTALL");
// 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))!; const item = inventory[payload.Category].find(item => item._id.toString() == (req.query.ItemId as string))!;
item.Features ??= 0; item.Features ??= 0;
item.Features |= EquipmentFeatures.INCARNON_GENESIS; item.Features |= EquipmentFeatures.INCARNON_GENESIS;
item.SkillTree = "0"; item.SkillTree = "0";
inventory.EvolutionProgress ??= []; inventory.EvolutionProgress ??= [];
if (!inventory.EvolutionProgress.find(entry => entry.ItemType == payload.EvoType)) { if (!inventory.EvolutionProgress.find(entry => entry.ItemType == payload.EvoType)) {
inventory.EvolutionProgress.push({ inventory.EvolutionProgress.push({
Progress: 0, Progress: 0,
Rank: 1, Rank: 1,
ItemType: payload.EvoType 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(); await inventory.save();
@ -34,7 +50,7 @@ export const evolveWeaponController: RequestHandler = async (req, res) => {
}; };
interface IEvolveWeaponRequest { interface IEvolveWeaponRequest {
Action: "EWA_INSTALL"; Action: string;
Category: WeaponTypeInternal; Category: WeaponTypeInternal;
Recipe: string; // e.g. "/Lotus/Types/Items/MiscItems/IncarnonAdapters/UnlockerBlueprints/DespairIncarnonBlueprint" Recipe: string; // e.g. "/Lotus/Types/Items/MiscItems/IncarnonAdapters/UnlockerBlueprints/DespairIncarnonBlueprint"
UninstallRecipe: ""; UninstallRecipe: "";

View File

@ -1,31 +1,28 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getSession } from "@/src/managers/sessionManager"; import { getSession } from "@/src/managers/sessionManager";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { IFindSessionRequest } from "@/src/types/session";
//TODO: cleanup export const findSessionsController: RequestHandler = (_req, res) => {
const findSessionsController: RequestHandler = (_req, res) => { const req = JSON.parse(String(_req.body)) as IFindSessionRequest;
const reqBody = JSON.parse(String(_req.body)); logger.debug("FindSession Request ", req);
logger.debug("FindSession Request ", { reqBody });
const req = JSON.parse(String(_req.body));
if (req.id != undefined) { if (req.id != undefined) {
logger.debug("Found ID"); 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 res.json({});
} else if (req.originalSessionId != undefined) { } else if (req.originalSessionId != undefined) {
logger.debug("Found OriginalSessionID"); logger.debug("Found OriginalSessionID");
const session = getSession(req.originalSessionId as string); const session = getSession(req.originalSessionId);
if (session) res.json({ queryId: req.queryId, Sessions: session }); if (session.length) res.json({ queryId: req.queryId, Sessions: session });
else res.json({}); else res.json({});
} else { } else {
logger.debug("Found SessionRequest"); logger.debug("Found SessionRequest");
const session = getSession(String(_req.body)); const session = getSession(req);
if (session) res.json({ queryId: req.queryId, Sessions: session }); if (session.length) res.json({ queryId: req.queryId, Sessions: session });
else res.json({}); 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,41 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory, addMiscItems, addEquipment } from "@/src/services/inventoryService"; import { getInventory, addMiscItems, addEquipment } from "@/src/services/inventoryService";
import { IMiscItem, TFocusPolarity } from "@/src/types/inventoryTypes/inventoryTypes"; import { IMiscItem, TFocusPolarity, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { ExportFocusUpgrades } from "warframe-public-export-plus"; import { ExportFocusUpgrades } from "warframe-public-export-plus";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const focusController: RequestHandler = async (req, res) => { export const focusController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
switch (req.query.op) { switch (req.query.op) {
default: default:
logger.error("Unhandled focus op type: " + req.query.op); logger.error("Unhandled focus op type: " + String(req.query.op));
logger.debug(req.body.toString()); logger.debug(String(req.body));
res.end(); res.end();
break; 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: { case FocusOperation.UnlockWay: {
const focusType = (JSON.parse(String(req.body)) as IWayRequest).FocusType; const focusType = (JSON.parse(String(req.body)) as IWayRequest).FocusType;
const focusPolarity = focusTypeToPolarity(focusType); const focusPolarity = focusTypeToPolarity(focusType);
@ -48,7 +70,7 @@ export const focusController: RequestHandler = async (req, res) => {
cost += ExportFocusUpgrades[focusType].baseFocusPointCost; cost += ExportFocusUpgrades[focusType].baseFocusPointCost;
inventory.FocusUpgrades.push({ ItemType: focusType, Level: 0 }); inventory.FocusUpgrades.push({ ItemType: focusType, Level: 0 });
} }
inventory.FocusXP[focusPolarity] -= cost; inventory.FocusXP![focusPolarity] -= cost;
await inventory.save(); await inventory.save();
res.json({ res.json({
FocusTypes: request.FocusTypes, FocusTypes: request.FocusTypes,
@ -66,7 +88,7 @@ export const focusController: RequestHandler = async (req, res) => {
const focusUpgradeDb = inventory.FocusUpgrades.find(entry => entry.ItemType == focusUpgrade.ItemType)!; const focusUpgradeDb = inventory.FocusUpgrades.find(entry => entry.ItemType == focusUpgrade.ItemType)!;
focusUpgradeDb.Level = focusUpgrade.Level; focusUpgradeDb.Level = focusUpgrade.Level;
} }
inventory.FocusXP[focusPolarity] -= cost; inventory.FocusXP![focusPolarity] -= cost;
await inventory.save(); await inventory.save();
res.json({ res.json({
FocusInfos: request.FocusInfos, FocusInfos: request.FocusInfos,
@ -81,15 +103,17 @@ export const focusController: RequestHandler = async (req, res) => {
"/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingChassis", "/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingChassis",
"/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingBarrel" "/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingBarrel"
]; ];
const result = await addEquipment("OperatorAmps", request.StartingWeaponType, accountId, parts); const inventory = await getInventory(accountId);
res.json(result); const inventoryChanges = addEquipment(inventory, "OperatorAmps", request.StartingWeaponType, parts);
await inventory.save();
res.json((inventoryChanges.OperatorAmps as IEquipmentClient[])[0]);
break; break;
} }
case FocusOperation.UnbindUpgrade: { case FocusOperation.UnbindUpgrade: {
const request = JSON.parse(String(req.body)) as IUnbindUpgradeRequest; const request = JSON.parse(String(req.body)) as IUnbindUpgradeRequest;
const focusPolarity = focusTypeToPolarity(request.FocusTypes[0]); const focusPolarity = focusTypeToPolarity(request.FocusTypes[0]);
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
inventory.FocusXP[focusPolarity] -= 750_000 * request.FocusTypes.length; inventory.FocusXP![focusPolarity] -= 750_000 * request.FocusTypes.length;
addMiscItems(inventory, [ addMiscItems(inventory, [
{ {
ItemType: "/Lotus/Types/Gameplay/Eidolon/Resources/SentientShards/SentientShardBrilliantItem", ItemType: "/Lotus/Types/Gameplay/Eidolon/Resources/SentientShards/SentientShardBrilliantItem",
@ -144,6 +168,7 @@ export const focusController: RequestHandler = async (req, res) => {
}; };
enum FocusOperation { enum FocusOperation {
InstallLens = "1",
UnlockWay = "2", UnlockWay = "2",
UnlockUpgrade = "3", UnlockUpgrade = "3",
LevelUpUpgrade = "4", LevelUpUpgrade = "4",
@ -186,6 +211,12 @@ interface ISentTrainingAmplifierRequest {
StartingWeaponType: string; StartingWeaponType: string;
} }
interface ILensInstallRequest {
LensType: string;
Category: TEquipmentKey;
WeaponId: string;
}
// Works for ways & upgrades // Works for ways & upgrades
const focusTypeToPolarity = (type: string): TFocusPolarity => { const focusTypeToPolarity = (type: string): TFocusPolarity => {
return ("AP_" + type.substr(1).split("/")[3].toUpperCase()) as 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,11 +7,11 @@ 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. // 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. // SpaceNinjaServer supports both endpoints right now.
// eslint-disable-next-line @typescript-eslint/no-misused-promises
const genericUpdateController: RequestHandler = async (request, response) => { const genericUpdateController: RequestHandler = async (request, response) => {
const accountId = await getAccountIdForRequest(request); 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)); await updateGeneric(update, accountId);
response.json(update);
}; };
export { genericUpdateController }; export { genericUpdateController };

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,6 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
const getFriendsController = (_request: Request, response: Response) => { const getFriendsController = (_request: Request, response: Response): void => {
response.writeHead(200, { response.writeHead(200, {
//Connection: "keep-alive", //Connection: "keep-alive",
//"Content-Encoding": "gzip", //"Content-Encoding": "gzip",

View File

@ -4,7 +4,6 @@ import { Guild } from "@/src/models/guildModel";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { toOid } from "@/src/helpers/inventoryHelpers"; import { toOid } from "@/src/helpers/inventoryHelpers";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
const getGuildController: RequestHandler = async (req, res) => { const getGuildController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await Inventory.findOne({ accountOwnerId: accountId }); const inventory = await Inventory.findOne({ accountOwnerId: accountId });

View File

@ -1,10 +1,8 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { Guild } from "@/src/models/guildModel"; import { Guild } from "@/src/models/guildModel";
import { IDojoClient, IDojoComponentClient } from "@/src/types/guildTypes"; import { getDojoClient } from "@/src/services/guildService";
import { toOid, toMongoDate } from "@/src/helpers/inventoryHelpers";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const getGuildDojoController: RequestHandler = async (req, res) => { export const getGuildDojoController: RequestHandler = async (req, res) => {
const guildId = req.query.guildId as string; const guildId = req.query.guildId as string;
@ -15,46 +13,16 @@ export const getGuildDojoController: RequestHandler = async (req, res) => {
} }
// Populate dojo info if not present // Populate dojo info if not present
if (!guild.DojoComponents || guild.DojoComponents.length == 0) { if (guild.DojoComponents.length == 0) {
guild.DojoComponents = [ guild.DojoComponents.push({
{ _id: new Types.ObjectId(),
_id: new Types.ObjectId(), pf: "/Lotus/Levels/ClanDojo/DojoHall.level",
pf: "/Lotus/Levels/ClanDojo/DojoHall.level", ppf: "",
ppf: "", CompletionTime: new Date(Date.now()),
CompletionTime: new Date(Date.now()) DecoCapacity: 600
} });
];
await guild.save(); await guild.save();
} }
const dojo: IDojoClient = { res.json(await getDojoClient(guild, 0));
_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);
}; };

View File

@ -1,13 +1,11 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
const getNewRewardSeedController: RequestHandler = (_req, res) => { export const getNewRewardSeedController: RequestHandler = (_req, res) => {
res.json({ rewardSeed: generateRewardSeed() }); res.json({ rewardSeed: generateRewardSeed() });
}; };
function generateRewardSeed(): number { export function generateRewardSeed(): number {
const min = -Number.MAX_SAFE_INTEGER; const min = -Number.MAX_SAFE_INTEGER;
const max = Number.MAX_SAFE_INTEGER; const max = Number.MAX_SAFE_INTEGER;
return Math.floor(Math.random() * (max - min + 1)) + min; 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 { getAccountIdForRequest } from "@/src/services/loginService";
import { getPersonalRooms } from "@/src/services/personalRoomsService"; import { getPersonalRooms } from "@/src/services/personalRoomsService";
import { getShip } from "@/src/services/shipService"; 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 { toOid } from "@/src/helpers/inventoryHelpers";
import { IGetShipResponse } from "@/src/types/shipTypes"; 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) => { export const getShipController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); 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 loadout = await getLoadout(accountId);
const ship = await getShip(personalRooms.activeShipId, "ShipInteriorColors ShipAttachments SkinFlavourItem"); const ship = await getShip(personalRoomsDb.activeShipId, "ShipAttachments SkinFlavourItem");
const getShipResponse: IGetShipResponse = { const getShipResponse: IGetShipResponse = {
ShipOwnerId: accountId, ShipOwnerId: accountId,
LoadOutInventory: { LoadOutPresets: loadout.toJSON() }, LoadOutInventory: { LoadOutPresets: loadout.toJSON() },
Ship: { Ship: {
...personalRooms.toJSON().Ship, ...personalRooms.Ship,
ShipId: toOid(personalRooms.activeShipId), ShipId: toOid(personalRoomsDb.activeShipId),
ShipInterior: { ShipInterior: {
Colors: ship.ShipInteriorColors, Colors: personalRooms.ShipInteriorColors,
ShipAttachments: ship.ShipAttachments, ShipAttachments: ship.ShipAttachments,
SkinFlavourItem: ship.SkinFlavourItem SkinFlavourItem: ship.SkinFlavourItem
} }
}, },
Apartment: personalRooms.Apartment Apartment: personalRooms.Apartment,
TailorShop: personalRooms.TailorShop
}; };
if (config.unlockAllShipFeatures) { if (config.unlockAllShipFeatures) {
@ -37,14 +38,3 @@ export const getShipController: RequestHandler = async (req, res) => {
res.json(getShipResponse); 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 { RequestHandler } from "express";
import ArchimedeanVendorManifest from "@/static/fixed_responses/getVendorInfo/ArchimedeanVendorManifest.json"; import { getVendorManifestByTypeName } from "@/src/services/serversideVendorsService";
import MaskSalesmanManifest from "@/static/fixed_responses/getVendorInfo/MaskSalesmanManifest.json";
import ZarimanCommisionsManifestArchimedean from "@/static/fixed_responses/getVendorInfo/ZarimanCommisionsManifestArchimedean.json";
export const getVendorInfoController: RequestHandler = (req, res) => { export const getVendorInfoController: RequestHandler = (req, res) => {
switch (req.query.vendor as string) { if (typeof req.query.vendor == "string") {
case "/Lotus/Types/Game/VendorManifests/Zariman/ArchimedeanVendorManifest": const manifest = getVendorManifestByTypeName(req.query.vendor);
res.json(ArchimedeanVendorManifest); if (!manifest) {
break;
case "/Lotus/Types/Game/VendorManifests/Ostron/MaskSalesmanManifest":
res.json(MaskSalesmanManifest);
break;
case "/Lotus/Types/Game/VendorManifests/Zariman/ZarimanCommisionsManifestArchimedean":
res.json(ZarimanCommisionsManifestArchimedean);
break;
default:
throw new Error(`Unknown vendor: ${req.query.vendor}`); 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

@ -24,23 +24,19 @@ interface IGildWeaponRequest {
// In export there no recipes for gild action, so reputation and ressources only consumed visually // 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) => { export const gildWeaponController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); 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); data.ItemId = String(req.query.ItemId);
if (!modularWeaponCategory.includes(req.query.Category as WeaponTypeInternal | "Hoverboards")) { 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"; data.Category = req.query.Category as WeaponTypeInternal | "Hoverboards";
const inventory = await getInventory(accountId); 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); const weaponIndex = inventory[data.Category].findIndex(x => String(x._id) === data.ItemId);
if (weaponIndex === -1) { 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]; const weapon = inventory[data.Category][weaponIndex];

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,96 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { InventoryDocumentProps } from "@/src/models/inventoryModels/inventoryModel";
import {
addEquipment,
addItem,
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);
addEquipment(inventory, "Suits", Suits[0].ItemType, undefined, inventoryChanges, { Configs: Suits[0].Configs });
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.InventoryChanges);
}
inventory.PlayedParkourTutorial = true;
inventory.ReceivedStartingGear = true;
return inventoryChanges;
};

View File

@ -1,5 +1,132 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getGuildForRequestEx, scaleRequiredCount } from "@/src/services/guildService";
import { ExportDojoRecipes, IDojoResearch } from "warframe-public-export-plus";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { addMiscItems, addRecipes, 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 { ITechProjectDatabase } from "@/src/types/guildTypes";
export const guildTechController: RequestHandler = (_req, res) => { export const guildTechController: RequestHandler = async (req, res) => {
res.status(500).end(); // This is what I got for a fresh clan. 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;
const action = data.Action.split(",")[0];
if (action == "Sync") {
res.json({
TechProjects: guild.toJSON().TechProjects
});
} else if (action == "Start") {
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
];
if (config.noDojoResearchCosts) {
processFundedProject(techProject, recipe);
}
}
await guild.save();
res.end();
} else if (action == "Contribute") {
const contributions = data as IGuildTechContributeFields;
const techProject = guild.TechProjects!.find(x => x.ItemType == contributions.RecipeType)!;
if (contributions.RegularCredits > techProject.ReqCredits) {
contributions.RegularCredits = techProject.ReqCredits;
}
techProject.ReqCredits -= contributions.RegularCredits;
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(techProject, recipe);
}
await guild.save();
await inventory.save();
res.json({
InventoryChanges: inventoryChanges
});
} else if (action == "Buy") {
const purchase = data as IGuildTechBuyFields;
const quantity = parseInt(data.Action.split(",")[1]);
const inventory = await getInventory(accountId);
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 {
throw new Error(`unknown guildTech action: ${data.Action}`);
}
}; };
const processFundedProject = (techProject: ITechProjectDatabase, recipe: IDojoResearch): void => {
techProject.State = 1;
techProject.CompletionDate = new Date(new Date().getTime() + (config.noDojoResearchTime ? 0 : recipe.time) * 1000);
};
type TGuildTechRequest = {
Action: string;
} & Partial<IGuildTechStartFields> &
Partial<IGuildTechContributeFields>;
interface IGuildTechStartFields {
Mode: "Guild";
RecipeType: string;
}
type IGuildTechBuyFields = IGuildTechStartFields;
interface IGuildTechContributeFields {
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 { logger } from "@/src/utils/logger";
import { ISession } from "@/src/types/session"; import { ISession } from "@/src/types/session";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
const hostSessionController: RequestHandler = async (req, res) => { const hostSessionController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const hostSessionRequest = JSON.parse(req.body as string) as ISession; const hostSessionRequest = JSON.parse(req.body as string) as ISession;

View File

@ -1,8 +1,87 @@
import { RequestHandler } from "express"; 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) => { export const inboxController: RequestHandler = async (req, res) => {
res.json(inbox); 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,31 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory, addMiscItems } from "@/src/services/inventoryService"; import { getInventory, addMiscItems, updateCurrency, addRecipes } from "@/src/services/inventoryService";
import { IOid } from "@/src/types/commonTypes"; import { IOid } from "@/src/types/commonTypes";
import {
IConsumedSuit,
IHelminthFoodRecord,
IInfestedFoundryClient,
IInfestedFoundryDatabase,
IInventoryClient,
IMiscItem,
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) => { export const infestedFoundryController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
switch (req.query.mode) { switch (req.query.mode) {
case "s": { case "s": {
// shard installation // shard installation
const request = getJSONfromString(String(req.body)) as IShardInstallRequest; const request = getJSONfromString<IShardInstallRequest>(String(req.body));
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const suit = inventory.Suits.find(suit => suit._id.toString() == request.SuitId.$oid)!; const suit = inventory.Suits.find(suit => suit._id.toString() == request.SuitId.$oid)!;
if (!suit.ArchonCrystalUpgrades || suit.ArchonCrystalUpgrades.length != 5) { if (!suit.ArchonCrystalUpgrades || suit.ArchonCrystalUpgrades.length != 5) {
@ -36,9 +51,51 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
break; 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)!;
// refund shard
const shard = Object.entries(colorToShard).find(
([color]) => color == suit.ArchonCrystalUpgrades![request.Slot].Color
)![1];
const miscItemChanges = [
{
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": { case "n": {
// name the beast // name the beast
const request = getJSONfromString(String(req.body)) as IHelminthNameRequest; const request = getJSONfromString<IHelminthNameRequest>(String(req.body));
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
inventory.InfestedFoundry ??= {}; inventory.InfestedFoundry ??= {};
inventory.InfestedFoundry.Name = request.newName; inventory.InfestedFoundry.Name = request.newName;
@ -53,13 +110,251 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
break; break;
} }
case "o": // offerings update case "c": {
// {"OfferingsIndex":540,"SuitTypes":["/Lotus/Powersuits/PaxDuviricus/PaxDuviricusBaseSuit","/Lotus/Powersuits/Nezha/NezhaBaseSuit","/Lotus/Powersuits/Devourer/DevourerBaseSuit"],"Extra":false} // consume items
res.status(404).end();
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(new Date().getTime() / 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; 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(
new Date().getTime() + 24 * 60 * 60 * 1000
);
const recipeChanges = addInfestedFoundryXP(inventory.InfestedFoundry!, 1600_00);
addRecipes(inventory, recipeChanges);
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(new Date().getTime() + 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: default:
throw new Error(`unhandled infestedFoundry mode: ${req.query.mode}`); throw new Error(`unhandled infestedFoundry mode: ${String(req.query.mode)}`);
} }
}; };
@ -70,21 +365,179 @@ interface IShardInstallRequest {
Color: string; Color: string;
} }
interface IShardUninstallRequest {
SuitId: IOid;
Slot: number;
}
interface IHelminthNameRequest { interface IHelminthNameRequest {
newName: string; newName: string;
} }
const colorToShard: Record<string, string> = { interface IHelminthFeedRequest {
ACC_RED: "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalAmar", ResourceContributions: {
ACC_RED_MYTHIC: "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalAmarMythic", ItemType: string;
ACC_YELLOW: "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalNira", Date: number; // unix timestamp
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", export const addInfestedFoundryXP = (infestedFoundry: IInfestedFoundryDatabase, delta: number): ITypeCount[] => {
ACC_GREEN_MYTHIC: "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalGreenMythic", const recipeChanges: ITypeCount[] = [];
ACC_ORANGE: "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalOrange", infestedFoundry.XP ??= 0;
ACC_ORANGE_MYTHIC: "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalOrangeMythic", const prevXP = infestedFoundry.XP;
ACC_PURPLE: "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalViolet", infestedFoundry.XP += delta;
ACC_PURPLE_MYTHIC: "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalVioletMythic" 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,84 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountForRequest } from "@/src/services/loginService";
import { toInventoryResponse } from "@/src/helpers/inventoryHelpers"; import { Inventory, TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
import { config } from "@/src/services/configService"; import { config } from "@/src/services/configService";
import allDialogue from "@/static/fixed_responses/allDialogue.json"; import allDialogue from "@/static/fixed_responses/allDialogue.json";
import allMissions from "@/static/fixed_responses/allMissions.json";
import { ILoadoutDatabase } from "@/src/types/saveLoadoutTypes"; import { ILoadoutDatabase } from "@/src/types/saveLoadoutTypes";
import { IInventoryDatabase, IShipInventory, equipmentKeys } from "@/src/types/inventoryTypes/inventoryTypes"; import { IInventoryClient, IShipInventory, equipmentKeys } from "@/src/types/inventoryTypes/inventoryTypes";
import { IPolarity, ArtifactPolarity } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { IPolarity, ArtifactPolarity, EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { ExportCustoms, ExportFlavour, ExportKeys, ExportResources } from "warframe-public-export-plus"; import {
ExportCustoms,
ExportFlavour,
ExportRegions,
ExportResources,
ExportVirtuals
} from "warframe-public-export-plus";
import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "./infestedFoundryController";
import { allDailyAffiliationKeys, createLibraryDailyTask } from "@/src/services/inventoryService";
// eslint-disable-next-line @typescript-eslint/no-misused-promises export const inventoryController: RequestHandler = async (request, response) => {
const inventoryController: RequestHandler = async (request, response) => { const account = await getAccountForRequest(request);
let accountId;
try {
accountId = await getAccountIdForRequest(request);
} catch (e) {
response.status(400).send("Log-in expired");
return;
}
const inventory = await Inventory.findOne({ accountOwnerId: accountId }) const inventory = await Inventory.findOne({ accountOwnerId: account._id.toString() });
.populate<{ LoadOutPresets: ILoadoutDatabase }>("LoadOutPresets")
.populate<{ Ships: IShipInventory }>("Ships", "-ShipInteriorColors");
if (!inventory) { if (!inventory) {
response.status(400).json({ error: "inventory was undefined" }); response.status(400).json({ error: "inventory was undefined" });
return; return;
} }
//TODO: make a function that converts from database representation to client // Handle daily reset
const inventoryJSON: IInventoryDatabase = inventory.toJSON(); const today: number = Math.trunc(new Date().getTime() / 86400000);
console.log(inventoryJSON.Ships); if (account.LastLoginDay != today) {
account.LastLoginDay = today;
await account.save();
const inventoryResponse = toInventoryResponse(inventoryJSON); for (const key of allDailyAffiliationKeys) {
inventory[key] = 16000 + inventory.PlayerLevel * 500;
}
inventory.DailyFocus = 250000 + inventory.PlayerLevel * 5000;
if (config.infiniteResources) { inventory.LibraryAvailableDailyTaskInfo = createLibraryDailyTask();
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.RegularCredits = 999999999;
inventoryResponse.TradesRemaining = 999999999; }
if (config.infinitePlatinum) {
inventoryResponse.PremiumCreditsFree = 999999999; inventoryResponse.PremiumCreditsFree = 999999999;
inventoryResponse.PremiumCredits = 999999999; inventoryResponse.PremiumCredits = 999999999;
} }
if (config.infiniteEndo) {
inventoryResponse.FusionPoints = 999999999;
}
if (config.infiniteRegalAya) {
inventoryResponse.PrimeTokens = 999999999;
}
if (config.skipAllDialogue) { if (config.skipAllDialogue) {
inventoryResponse.TauntHistory = [ inventoryResponse.TauntHistory = [
@ -55,38 +93,17 @@ const inventoryController: RequestHandler = async (request, response) => {
} }
if (config.unlockAllMissions) { 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"); 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) { if (config.unlockAllShipDecorations) {
inventoryResponse.ShipDecorations = []; inventoryResponse.ShipDecorations = [];
for (const [uniqueName, item] of Object.entries(ExportResources)) { for (const [uniqueName, item] of Object.entries(ExportResources)) {
@ -104,20 +121,32 @@ const inventoryController: RequestHandler = async (request, response) => {
} }
if (config.unlockAllSkins) { if (config.unlockAllSkins) {
inventoryResponse.WeaponSkins = []; const missingWeaponSkins = new Set(Object.keys(ExportCustoms));
for (const uniqueName in ExportCustoms) { inventoryResponse.WeaponSkins.forEach(x => missingWeaponSkins.delete(x.ItemType));
for (const uniqueName of missingWeaponSkins) {
inventoryResponse.WeaponSkins.push({ inventoryResponse.WeaponSkins.push({
ItemId: { ItemId: {
$oid: "000000000000000000000000" $oid: "ca70ca70ca70ca70" + catBreadHash(uniqueName).toString(16).padStart(8, "0")
}, },
ItemType: uniqueName 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) { if (typeof config.spoofMasteryRank === "number" && config.spoofMasteryRank >= 0) {
inventoryResponse.PlayerLevel = config.spoofMasteryRank; 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. // This client has not been patched to accept any mastery rank, need to fake the XP.
inventoryResponse.XPInfo = []; inventoryResponse.XPInfo = [];
let numFrames = getExpRequiredForMr(Math.min(config.spoofMasteryRank, 5030)) / 6000; let numFrames = getExpRequiredForMr(Math.min(config.spoofMasteryRank, 5030)) / 6000;
@ -132,7 +161,7 @@ const inventoryController: RequestHandler = async (request, response) => {
if (config.universalPolarityEverywhere) { if (config.universalPolarityEverywhere) {
const Polarity: IPolarity[] = []; const Polarity: IPolarity[] = [];
for (let i = 0; i != 10; ++i) { for (let i = 0; i != 12; ++i) {
Polarity.push({ Polarity.push({
Slot: i, Slot: i,
Value: ArtifactPolarity.Any Value: ArtifactPolarity.Any
@ -147,13 +176,62 @@ const inventoryController: RequestHandler = async (request, response) => {
} }
} }
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;
}
}
}
}
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) {
for (const key of allDailyAffiliationKeys) {
inventoryResponse[key] = 999_999;
}
}
if (inventoryResponse.InfestedFoundry) {
applyCheatsToInfestedFoundry(inventoryResponse.InfestedFoundry);
}
// Fix for #380 // Fix for #380
inventoryResponse.NextRefill = { $date: { $numberLong: "9999999999999" } }; inventoryResponse.NextRefill = { $date: { $numberLong: "9999999999999" } };
response.json(inventoryResponse); // This determines if the "void fissures" tab is shown in navigation.
inventoryResponse.HasOwnedVoidProjectionsPreviously = true;
// Omitting this field so opening the navigation resyncs the inventory which is more desirable for typical usage.
//inventoryResponse.LastInventorySync = toOid(new Types.ObjectId());
return inventoryResponse;
}; };
const addString = (arr: string[], str: string): void => { export const addString = (arr: string[], str: string): void => {
if (!arr.find(x => x == str)) { if (!arr.find(x => x == str)) {
arr.push(str); arr.push(str);
} }
@ -166,4 +244,29 @@ const getExpRequiredForMr = (rank: number): number => {
return 2_250_000 + 147_500 * (rank - 30); 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;
}
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.
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 { getAccountIdForRequest } from "@/src/services/loginService";
import { updateCurrency } from "@/src/services/inventoryService"; import { getInventory, updateCurrency } from "@/src/services/inventoryService";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { updateSlots } from "@/src/services/inventoryService"; import { updateSlots } from "@/src/services/inventoryService";
import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger";
/* /*
loadout slots are additionally purchased slots only loadout slots are additionally purchased slots only
@ -18,19 +19,22 @@ import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
number of frames = extra - slots + 2 number of frames = extra - slots + 2
*/ */
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const inventorySlotsController: RequestHandler = async (req, res) => { export const inventorySlotsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); 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 inventory = await getInventory(accountId);
const currencyChanges = updateCurrency(inventory, 20, true);
const currencyChanges = await updateCurrency(20, true, accountId); updateSlots(inventory, body.Bin, 1, 1);
await updateSlots(accountId, InventorySlot.PVE_LOADOUTS, 1, 1); await inventory.save();
//console.log({ InventoryChanges: currencyChanges }, " added loadout changes:");
res.json({ InventoryChanges: currencyChanges }); res.json({ InventoryChanges: currencyChanges });
}; };
interface IInventorySlotsRequest {
Bin: InventorySlot;
}

View File

@ -1,55 +1,50 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { config } from "@/src/services/configService"; 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 { Account } from "@/src/models/loginModel";
import { createAccount, isCorrectPassword } from "@/src/services/loginService"; import { createAccount, isCorrectPassword, isNameTaken } from "@/src/services/loginService";
import { ILoginResponse } from "@/src/types/loginTypes"; import { IDatabaseAccountJson, ILoginRequest, ILoginResponse } from "@/src/types/loginTypes";
import { DTLS, groups, HUB, platformCDNs } from "@/static/fixed_responses/login_static";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
// eslint-disable-next-line @typescript-eslint/no-misused-promises export const loginController: RequestHandler = async (request, response) => {
const loginController: RequestHandler = async (request, response) => { const loginRequest = JSON.parse(String(request.body)) as ILoginRequest; // parse octet stream of json data to json object
// 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);
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 nonce = Math.round(Math.random() * Number.MAX_SAFE_INTEGER);
const buildLabel: string =
typeof request.query.buildLabel == "string"
? request.query.buildLabel.split(" ").join("+")
: buildConfig.buildLabel;
if (!account && config.autoCreateAccount && loginRequest.ClientType != "webui") { if (!account && config.autoCreateAccount && loginRequest.ClientType != "webui") {
try { 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({ const newAccount = await createAccount({
email: loginRequest.email, email: loginRequest.email,
password: loginRequest.password, password: loginRequest.password,
DisplayName: loginRequest.email.substring(0, loginRequest.email.indexOf("@")), DisplayName: name,
CountryCode: loginRequest.lang.toUpperCase(), CountryCode: loginRequest.lang.toUpperCase(),
ClientType: loginRequest.ClientType, ClientType: loginRequest.ClientType,
CrossPlatformAllowed: true, CrossPlatformAllowed: true,
ForceLogoutVersion: 0, ForceLogoutVersion: 0,
ConsentNeeded: false, ConsentNeeded: false,
TrackedSettings: [], TrackedSettings: [],
Nonce: nonce Nonce: nonce,
LatestEventMessageDate: new Date(0)
}); });
logger.debug("created new account"); logger.debug("created new account");
// eslint-disable-next-line @typescript-eslint/no-unused-vars response.json(createLoginResponse(newAccount, buildLabel));
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);
return; return;
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error) { if (error instanceof Error) {
@ -72,20 +67,29 @@ const loginController: RequestHandler = async (request, response) => {
} }
await account.save(); await account.save();
const { email, password, ...databaseAccount } = account.toJSON(); response.json(createLoginResponse(account.toJSON(), buildLabel));
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);
}; };
export { loginController }; const createLoginResponse = (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 ?? [config.myAddress],
platformCDNs: config.platformCDNs,
HUB: config.hubAddress,
NRS: config.NRS,
DTLS: 99,
BuildLabel: buildLabel,
MatchmakingBuildId: buildConfig.matchmakingBuildId
};
};

View File

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

View File

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

View File

@ -2,10 +2,21 @@ import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
import { getInventory, updateCurrency, addEquipment, addMiscItems } from "@/src/services/inventoryService"; import {
getInventory,
updateCurrency,
addEquipment,
addMiscItems,
applyDefaultUpgrades
} from "@/src/services/inventoryService";
import { ExportWeapons } from "warframe-public-export-plus";
const modularWeaponTypes: Record<string, TEquipmentKey> = { const modularWeaponTypes: Record<string, TEquipmentKey> = {
"/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary": "LongGuns",
"/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryBeam": "LongGuns", "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryBeam": "LongGuns",
"/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryLauncher": "LongGuns",
"/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryShotgun": "LongGuns",
"/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimarySniper": "LongGuns",
"/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary": "Pistols", "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary": "Pistols",
"/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryBeam": "Pistols", "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryBeam": "Pistols",
"/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryShotgun": "Pistols", "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryShotgun": "Pistols",
@ -23,26 +34,22 @@ interface IModularCraftRequest {
Parts: string[]; Parts: string[];
} }
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const modularWeaponCraftingController: RequestHandler = async (req, res) => { export const modularWeaponCraftingController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); 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)) { if (!(data.WeaponType in modularWeaponTypes)) {
throw new Error(`unknown modular weapon type: ${data.WeaponType}`); throw new Error(`unknown modular weapon type: ${data.WeaponType}`);
} }
const category = modularWeaponTypes[data.WeaponType]; const category = modularWeaponTypes[data.WeaponType];
const inventory = await getInventory(accountId);
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const configs = applyDefaultUpgrades(inventory, ExportWeapons[data.Parts[0]]?.defaultUpgrades);
// Give weapon // Give weapon
const weapon = await addEquipment(category, data.WeaponType, accountId, data.Parts); const inventoryChanges = addEquipment(inventory, category, data.WeaponType, data.Parts, {}, { Configs: configs });
// Remove credits // Remove credits & parts
const currencyChanges = await updateCurrency(
category == "Hoverboards" || category == "MoaPets" ? 5000 : 4000,
false,
accountId
);
// Remove parts
const miscItemChanges = []; const miscItemChanges = [];
for (const part of data.Parts) { for (const part of data.Parts) {
miscItemChanges.push({ miscItemChanges.push({
@ -50,15 +57,19 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res)
ItemCount: -1 ItemCount: -1
}); });
} }
const inventory = await getInventory(accountId); const currencyChanges = updateCurrency(
inventory,
category == "Hoverboards" || category == "MoaPets" ? 5000 : 4000,
false
);
addMiscItems(inventory, miscItemChanges); addMiscItems(inventory, miscItemChanges);
await inventory.save(); await inventory.save();
// Tell client what we did // Tell client what we did
res.json({ res.json({
InventoryChanges: { InventoryChanges: {
...inventoryChanges,
...currencyChanges, ...currencyChanges,
[category]: [weapon],
MiscItems: miscItemChanges MiscItems: miscItemChanges
} }
}); });

View File

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

View File

@ -0,0 +1,43 @@
import { getDojoClient, getGuildForRequest } from "@/src/services/guildService";
import { RequestHandler } from "express";
import { Types } from "mongoose";
import { ExportDojoRecipes } from "warframe-public-export-plus";
export const placeDecoInComponentController: RequestHandler = async (req, res) => {
const guild = await getGuildForRequest(req);
const request = JSON.parse(String(req.body)) as IPlaceDecoInComponentRequest;
// At this point, we know that a member of the guild is making this request. Assuming they are allowed to place decorations.
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);
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 { addMiscItems, getInventory } from "@/src/services/inventoryService";
import { ExportRelics, IRelic } from "warframe-public-export-plus"; import { ExportRelics, IRelic } from "warframe-public-export-plus";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const projectionManagerController: RequestHandler = async (req, res) => { export const projectionManagerController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId); 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"] // e.g. "/Lotus/Types/Game/Projections/T2VoidProjectionProteaPrimeDBronze" -> ["Lith", "W5", "VPQ_BRONZE"]
const parseProjection = (typeName: string): [string, string, VoidProjectionQuality] => { const parseProjection = (typeName: string): [string, string, VoidProjectionQuality] => {
const relic: IRelic | undefined = ExportRelics[typeName]; const relic: IRelic | undefined = ExportRelics[typeName];
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!relic) { if (!relic) {
throw new Error(`Unknown projection ${typeName}`); throw new Error(`Unknown projection ${typeName}`);
} }

View File

@ -1,12 +1,14 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService"; 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 { 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) => { 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 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); res.json(response);
}; };

View File

@ -1,16 +1,15 @@
import { getGuildForRequest } from "@/src/services/guildService"; import { config } from "@/src/services/configService";
import { getDojoClient, getGuildForRequest } from "@/src/services/guildService";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const queueDojoComponentDestructionController: RequestHandler = async (req, res) => { export const queueDojoComponentDestructionController: RequestHandler = async (req, res) => {
const guild = await getGuildForRequest(req); const guild = await getGuildForRequest(req);
const componentId = req.query.componentId as string; const componentId = req.query.componentId as string;
guild.DojoComponents!.splice(
guild.DojoComponents!.findIndex(x => x._id.toString() === componentId), guild.DojoComponents.id(componentId)!.DestructionTime = new Date(
1 Date.now() + (config.fastDojoRoomDestruction ? 5_000 : 2 * 3600_000)
); );
await guild.save(); await guild.save();
res.json({ res.json(await getDojoClient(guild, 0, componentId));
DojoRequestStatus: 1
});
}; };

View File

@ -1,9 +1,87 @@
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express"; 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/rivenFingerprintHelper";
import { ExportUpgrades } from "warframe-public-export-plus";
import { IOid } from "@/src/types/commonTypes";
const rerollRandomModController: RequestHandler = (_req, res) => { export const rerollRandomModController: RequestHandler = async (req, res) => {
logger.debug("RerollRandomMod Request", { info: _req.body.toString("hex").replace(/(.)(.)/g, "$1$2 ") }); const accountId = await getAccountIdForRequest(req);
res.json({}); 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,85 @@
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: string[]; // unsure
}

View File

@ -4,7 +4,6 @@ import { handleInventoryItemConfigChange } from "@/src/services/saveLoadoutServi
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const saveLoadoutController: RequestHandler = async (req, res) => { export const saveLoadoutController: RequestHandler = async (req, res) => {
//validate here //validate here
const accountId = await getAccountIdForRequest(req); 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,7 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { ISellRequest } from "@/src/types/sellTypes";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory, addMods, addRecipes } from "@/src/services/inventoryService"; import { getInventory, addMods, addRecipes, addMiscItems, addConsumables } from "@/src/services/inventoryService";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const sellController: RequestHandler = async (req, res) => { export const sellController: RequestHandler = async (req, res) => {
const payload = JSON.parse(String(req.body)) as ISellRequest; const payload = JSON.parse(String(req.body)) as ISellRequest;
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
@ -14,6 +12,20 @@ export const sellController: RequestHandler = async (req, res) => {
inventory.RegularCredits += payload.SellPrice; inventory.RegularCredits += payload.SellPrice;
} else if (payload.SellCurrency == "SC_FusionPoints") { } else if (payload.SellCurrency == "SC_FusionPoints") {
inventory.FusionPoints += payload.SellPrice; 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 { } else {
throw new Error("Unknown SellCurrency: " + payload.SellCurrency); throw new Error("Unknown SellCurrency: " + payload.SellCurrency);
} }
@ -39,6 +51,56 @@ export const sellController: RequestHandler = async (req, res) => {
inventory.Melee.pull({ _id: sellItem.String }); inventory.Melee.pull({ _id: sellItem.String });
}); });
} }
if (payload.Items.SpaceSuits) {
payload.Items.SpaceSuits.forEach(sellItem => {
inventory.SpaceSuits.pull({ _id: sellItem.String });
});
}
if (payload.Items.SpaceGuns) {
payload.Items.SpaceGuns.forEach(sellItem => {
inventory.SpaceGuns.pull({ _id: sellItem.String });
});
}
if (payload.Items.SpaceMelee) {
payload.Items.SpaceMelee.forEach(sellItem => {
inventory.SpaceMelee.pull({ _id: sellItem.String });
});
}
if (payload.Items.Sentinels) {
payload.Items.Sentinels.forEach(sellItem => {
inventory.Sentinels.pull({ _id: sellItem.String });
});
}
if (payload.Items.SentinelWeapons) {
payload.Items.SentinelWeapons.forEach(sellItem => {
inventory.SentinelWeapons.pull({ _id: sellItem.String });
});
}
if (payload.Items.OperatorAmps) {
payload.Items.OperatorAmps.forEach(sellItem => {
inventory.OperatorAmps.pull({ _id: sellItem.String });
});
}
if (payload.Items.Hoverboards) {
payload.Items.Hoverboards.forEach(sellItem => {
inventory.Hoverboards.pull({ _id: sellItem.String });
});
}
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) { if (payload.Items.Recipes) {
const recipeChanges = []; const recipeChanges = [];
for (const sellItem of payload.Items.Recipes) { for (const sellItem of payload.Items.Recipes) {
@ -63,7 +125,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(); await inventory.save();
res.json({}); 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"; import { RequestHandler } from "express";
const setActiveQuestController: RequestHandler = (_req, res) => { export const setActiveQuestController: RequestHandler<
res.sendStatus(200); 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 { RequestHandler } from "express";
import { Types } from "mongoose"; import { Types } from "mongoose";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const setActiveShipController: RequestHandler = async (req, res) => { export const setActiveShipController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const shipId = parseString(req.query.shipId); const shipId = parseString(req.query.shipId);

View File

@ -2,12 +2,23 @@ import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { getPersonalRooms } from "@/src/services/personalRoomsService"; import { getPersonalRooms } from "@/src/services/personalRoomsService";
import { TBootLocation } from "@/src/types/shipTypes"; 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) => { export const setBootLocationController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const personalRooms = await getPersonalRooms(accountId); const personalRooms = await getPersonalRooms(accountId);
personalRooms.Ship.BootLocation = req.query.bootLocation as string as TBootLocation; personalRooms.Ship.BootLocation = req.query.bootLocation as string as TBootLocation;
await personalRooms.save(); 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(); 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 };

View File

@ -0,0 +1,17 @@
import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory } from "@/src/services/inventoryService";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
export const setEquippedInstrumentController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const body = getJSONfromString<ISetEquippedInstrumentRequest>(String(req.body));
inventory.EquippedInstrument = body.Instrument;
await inventory.save();
res.end();
};
interface ISetEquippedInstrumentRequest {
Instrument: string;
}

View File

@ -0,0 +1,11 @@
import { getAccountIdForRequest } from "@/src/services/loginService";
import { ISetPlacedDecoInfoRequest } from "@/src/types/shipTypes";
import { RequestHandler } from "express";
import { handleSetPlacedDecoInfo } from "@/src/services/shipCustomizationsService";
export const setPlacedDecoInfoController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const payload = JSON.parse(req.body as string) as ISetPlacedDecoInfoRequest;
await handleSetPlacedDecoInfo(accountId, payload);
res.end();
};

View File

@ -1,14 +1,15 @@
import { getAccountIdForRequest } from "@/src/services/loginService";
import { setShipCustomizations } from "@/src/services/shipCustomizationsService"; import { setShipCustomizations } from "@/src/services/shipCustomizationsService";
import { ISetShipCustomizationsRequest } from "@/src/types/shipTypes"; import { ISetShipCustomizationsRequest } from "@/src/types/shipTypes";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const setShipCustomizationsController: RequestHandler = async (req, res) => { export const setShipCustomizationsController: RequestHandler = async (req, res) => {
try { try {
const accountId = await getAccountIdForRequest(req);
const setShipCustomizationsRequest = JSON.parse(req.body as string) as ISetShipCustomizationsRequest; const setShipCustomizationsRequest = JSON.parse(req.body as string) as ISetShipCustomizationsRequest;
const setShipCustomizationsResponse = await setShipCustomizations(setShipCustomizationsRequest); const setShipCustomizationsResponse = await setShipCustomizations(accountId, setShipCustomizationsRequest);
res.json(setShipCustomizationsResponse); res.json(setShipCustomizationsResponse);
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error) { if (error instanceof Error) {

View File

@ -0,0 +1,31 @@
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
import { getPersonalRooms } from "@/src/services/personalRoomsService";
import { IOid } from "@/src/types/commonTypes";
import { Types } from "mongoose";
export const setShipFavouriteLoadoutController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const personalRooms = await getPersonalRooms(accountId);
const body = JSON.parse(String(req.body)) as ISetShipFavouriteLoadoutRequest;
if (body.BootLocation != "SHOP") {
throw new Error(`unexpected BootLocation: ${body.BootLocation}`);
}
const display = personalRooms.TailorShop.FavouriteLoadouts.find(x => x.Tag == body.TagName);
if (display) {
display.LoadoutId = new Types.ObjectId(body.FavouriteLoadoutId.$oid);
} else {
personalRooms.TailorShop.FavouriteLoadouts.push({
Tag: body.TagName,
LoadoutId: new Types.ObjectId(body.FavouriteLoadoutId.$oid)
});
}
await personalRooms.save();
res.json({});
};
interface ISetShipFavouriteLoadoutRequest {
BootLocation: string;
FavouriteLoadoutId: IOid;
TagName: string;
}

View File

@ -2,7 +2,6 @@ import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const setSupportedSyndicateController: RequestHandler = async (req, res) => { export const setSupportedSyndicateController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);

View File

@ -4,11 +4,10 @@ import { getInventory } from "@/src/services/inventoryService";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { WeaponTypeInternal } from "@/src/services/itemDataService"; import { WeaponTypeInternal } from "@/src/services/itemDataService";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const setWeaponSkillTreeController: RequestHandler = async (req, res) => { export const setWeaponSkillTreeController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const payload = getJSONfromString(String(req.body)) as ISetWeaponSkillTreeRequest; const payload = getJSONfromString<ISetWeaponSkillTreeRequest>(String(req.body));
const item = inventory[req.query.Category as WeaponTypeInternal].find( const item = inventory[req.query.Category as WeaponTypeInternal].find(
item => item._id.toString() == (req.query.ItemId as string) item => item._id.toString() == (req.query.ItemId as string)

View File

@ -4,7 +4,6 @@ import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { handleSetShipDecorations } from "@/src/services/shipCustomizationsService"; import { handleSetShipDecorations } from "@/src/services/shipCustomizationsService";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const shipDecorationsController: RequestHandler = async (req, res) => { export const shipDecorationsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const shipDecorationsRequest = JSON.parse(req.body as string) as IShipDecorationsRequest; const shipDecorationsRequest = JSON.parse(req.body as string) as IShipDecorationsRequest;
@ -14,7 +13,7 @@ export const shipDecorationsController: RequestHandler = async (req, res) => {
res.send(placedDecoration); res.send(placedDecoration);
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error) { if (error instanceof Error) {
logger.error(`error in saveLoadoutController: ${error.message}`); logger.error(`error in shipDecorationsController: ${error.message}`);
res.status(400).json({ error: error.message }); res.status(400).json({ error: error.message });
} }
} }

View File

@ -0,0 +1,27 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IIncentiveState } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express";
export const startCollectibleEntryController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const request = getJSONfromString<IStartCollectibleEntryRequest>(String(req.body));
inventory.CollectibleSeries ??= [];
inventory.CollectibleSeries.push({
CollectibleType: request.target,
Count: 0,
Tracking: "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
ReqScans: request.reqScans,
IncentiveStates: request.other
});
await inventory.save();
res.status(200).end();
};
interface IStartCollectibleEntryRequest {
target: string;
reqScans: number;
other: IIncentiveState[];
}

View File

@ -1,29 +1,41 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { IDojoComponentClient } from "@/src/types/guildTypes"; import { IDojoComponentClient } from "@/src/types/guildTypes";
import { getGuildForRequest } from "@/src/services/guildService"; import { getDojoClient, getGuildForRequest } from "@/src/services/guildService";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { ExportDojoRecipes } from "warframe-public-export-plus";
import { config } from "@/src/services/configService";
interface IStartDojoRecipeRequest { interface IStartDojoRecipeRequest {
PlacedComponent: IDojoComponentClient; PlacedComponent: IDojoComponentClient;
Revision: number; Revision: number;
} }
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const startDojoRecipeController: RequestHandler = async (req, res) => { export const startDojoRecipeController: RequestHandler = async (req, res) => {
const guild = await getGuildForRequest(req); 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 start a build. // At this point, we know that a member of the guild is making this request. Assuming they are allowed to start a build.
const request = JSON.parse(String(req.body)) as IStartDojoRecipeRequest; const request = JSON.parse(String(req.body)) as IStartDojoRecipeRequest;
guild.DojoComponents!.push({
_id: new Types.ObjectId(), const room = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == request.PlacedComponent.pf);
pf: request.PlacedComponent.pf, if (room) {
ppf: request.PlacedComponent.ppf, guild.DojoCapacity += room.capacity;
pi: new Types.ObjectId(request.PlacedComponent.pi!.$oid), guild.DojoEnergy += room.energy;
op: request.PlacedComponent.op, }
pp: request.PlacedComponent.pp,
CompletionTime: new Date(Date.now()) // TOOD: Omit this field & handle the "Collecting Materials" state. const component =
}); guild.DojoComponents[
guild.DojoComponents.push({
_id: new Types.ObjectId(),
pf: request.PlacedComponent.pf,
ppf: request.PlacedComponent.ppf,
pi: new Types.ObjectId(request.PlacedComponent.pi!.$oid),
op: request.PlacedComponent.op,
pp: request.PlacedComponent.pp,
DecoCapacity: room?.decoCapacity
}) - 1
];
if (config.noDojoRoomBuildStage) {
component.CompletionTime = new Date(Date.now());
}
await guild.save(); await guild.save();
res.json({ res.json(await getDojoClient(guild, 0));
DojoRequestStatus: 0
});
}; };

View File

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

View File

@ -0,0 +1,14 @@
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const startLibraryPersonalTargetController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
inventory.LibraryPersonalTarget = req.query.target as string;
await inventory.save();
res.json({
IsQuest: false,
Target: req.query.target
});
};

View File

@ -1,21 +1,123 @@
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { startRecipe } from "@/src/services/recipeService";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getRecipe } from "@/src/services/itemDataService";
import { addMiscItems, getInventory, updateCurrency } from "@/src/services/inventoryService";
import { unixTimesInMs } from "@/src/constants/timeConstants";
import { Types } from "mongoose";
import { ISpectreLoadout } from "@/src/types/inventoryTypes/inventoryTypes";
import { toOid } from "@/src/helpers/inventoryHelpers";
import { ExportWeapons } from "warframe-public-export-plus";
interface IStartRecipeRequest { interface IStartRecipeRequest {
RecipeName: string; RecipeName: string;
Ids: string[]; Ids: string[];
} }
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const startRecipeController: RequestHandler = async (req, res) => { export const startRecipeController: RequestHandler = async (req, res) => {
const startRecipeRequest = getJSONfromString(String(req.body)) as IStartRecipeRequest; const startRecipeRequest = getJSONfromString<IStartRecipeRequest>(String(req.body));
logger.debug("StartRecipe Request", { startRecipeRequest }); logger.debug("StartRecipe Request", { startRecipeRequest });
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const newRecipeId = await startRecipe(startRecipeRequest.RecipeName, accountId); const recipeName = startRecipeRequest.RecipeName;
res.json(newRecipeId); const recipe = getRecipe(recipeName);
if (!recipe) {
throw new Error(`unknown recipe ${recipeName}`);
}
const inventory = await getInventory(accountId);
updateCurrency(inventory, recipe.buildPrice, false);
const pr =
inventory.PendingRecipes[
inventory.PendingRecipes.push({
ItemType: recipeName,
CompletionDate: new Date(Date.now() + recipe.buildTime * unixTimesInMs.second),
_id: new Types.ObjectId()
}) - 1
];
for (let i = 0; i != recipe.ingredients.length; ++i) {
if (startRecipeRequest.Ids[i]) {
const category = ExportWeapons[recipe.ingredients[i].ItemType].productCategory;
if (category != "LongGuns" && category != "Pistols" && category != "Melee") {
throw new Error(`unexpected equipment ingredient type: ${category}`);
}
const equipmentIndex = inventory[category].findIndex(x => x._id.equals(startRecipeRequest.Ids[i]));
if (equipmentIndex == -1) {
throw new Error(`could not find equipment item to use for recipe`);
}
pr[category] ??= [];
pr[category].push(inventory[category][equipmentIndex]);
inventory[category].splice(equipmentIndex, 1);
} else {
addMiscItems(inventory, [
{
ItemType: recipe.ingredients[i].ItemType,
ItemCount: recipe.ingredients[i].ItemCount * -1
}
]);
}
}
if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") {
const spectreLoadout: ISpectreLoadout = {
ItemType: recipe.resultType,
Suits: "",
LongGuns: "",
Pistols: "",
Melee: ""
};
for (
let secretIngredientsIndex = 0;
secretIngredientsIndex != recipe.secretIngredients!.length;
++secretIngredientsIndex
) {
const type = recipe.secretIngredients![secretIngredientsIndex].ItemType;
const oid = startRecipeRequest.Ids[recipe.ingredients.length + secretIngredientsIndex];
if (oid == "ffffffffffffffffffffffff") {
// user chose to preserve the active loadout
break;
}
if (type == "/Lotus/Types/Game/PowerSuits/PlayerPowerSuit") {
const item = inventory.Suits.id(oid)!;
spectreLoadout.Suits = item.ItemType;
} else if (type == "/Lotus/Weapons/Tenno/Pistol/LotusPistol") {
const item = inventory.Pistols.id(oid)!;
spectreLoadout.Pistols = item.ItemType;
spectreLoadout.PistolsModularParts = item.ModularParts;
} else if (type == "/Lotus/Weapons/Tenno/LotusLongGun") {
const item = inventory.LongGuns.id(oid)!;
spectreLoadout.LongGuns = item.ItemType;
spectreLoadout.LongGunsModularParts = item.ModularParts;
} else {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
console.assert(type == "/Lotus/Types/Game/LotusMeleeWeapon");
const item = inventory.Melee.id(oid)!;
spectreLoadout.Melee = item.ItemType;
spectreLoadout.MeleeModularParts = item.ModularParts;
}
}
if (
spectreLoadout.Suits != "" &&
spectreLoadout.LongGuns != "" &&
spectreLoadout.Pistols != "" &&
spectreLoadout.Melee != ""
) {
inventory.PendingSpectreLoadouts ??= [];
const existingIndex = inventory.PendingSpectreLoadouts.findIndex(x => x.ItemType == recipe.resultType);
if (existingIndex != -1) {
inventory.PendingSpectreLoadouts.splice(existingIndex, 1);
}
inventory.PendingSpectreLoadouts.push(spectreLoadout);
logger.debug("pending spectre loadout", spectreLoadout);
}
}
await inventory.save();
res.json({ RecipeId: toOid(pr._id) });
}; };

View File

@ -3,7 +3,6 @@ import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { IStepSequencer } from "@/src/types/inventoryTypes/inventoryTypes"; import { IStepSequencer } from "@/src/types/inventoryTypes/inventoryTypes";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const stepSequencersController: RequestHandler = async (req, res) => { export const stepSequencersController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);

View File

@ -1,25 +1,84 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { syndicateSacrifice } from "@/src/services/inventoryService";
import { ISyndicateSacrifice } from "@/src/types/syndicateTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { ExportSyndicates, ISyndicateSacrifice } from "warframe-public-export-plus";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { addMiscItems, combineInventoryChanges, getInventory, updateCurrency } from "@/src/services/inventoryService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
// eslint-disable-next-line @typescript-eslint/no-misused-promises export const syndicateSacrificeController: RequestHandler = async (request, response) => {
const syndicateSacrificeController: RequestHandler = async (request, response) => {
const accountId = await getAccountIdForRequest(request); const accountId = await getAccountIdForRequest(request);
const update = getJSONfromString(String(request.body)) as ISyndicateSacrifice; const inventory = await getInventory(accountId);
let reply = {}; const data = getJSONfromString<ISyndicateSacrificeRequest>(String(request.body));
try {
if (typeof update !== "object") {
throw new Error("Invalid data format");
}
reply = await syndicateSacrifice(update, accountId); let syndicate = inventory.Affiliations.find(x => x.Tag == data.AffiliationTag);
} catch (err) { if (!syndicate) {
console.error("Error parsing JSON data:", err); syndicate = inventory.Affiliations[inventory.Affiliations.push({ Tag: data.AffiliationTag, Standing: 0 }) - 1];
} }
response.json(reply); const level = data.SacrificeLevel - (syndicate.Title ?? 0);
const res: ISyndicateSacrificeResponse = {
AffiliationTag: data.AffiliationTag,
InventoryChanges: {},
Level: data.SacrificeLevel,
LevelIncrease: level <= 0 ? 1 : level,
NewEpisodeReward: syndicate.Tag == "RadioLegionIntermission9Syndicate"
};
const manifest = ExportSyndicates[data.AffiliationTag];
let sacrifice: ISyndicateSacrifice | undefined;
let reward: string | undefined;
if (data.SacrificeLevel == 0) {
sacrifice = manifest.initiationSacrifice;
reward = manifest.initiationReward;
syndicate.Initiated = true;
} else {
sacrifice = manifest.titles?.find(x => x.level == data.SacrificeLevel)?.sacrifice;
}
if (sacrifice) {
res.InventoryChanges = { ...updateCurrency(inventory, sacrifice.credits, false) };
const miscItemChanges = sacrifice.items.map(x => ({
ItemType: x.ItemType,
ItemCount: x.ItemCount * -1
}));
addMiscItems(inventory, miscItemChanges);
res.InventoryChanges.MiscItems = miscItemChanges;
}
syndicate.Title ??= 0;
syndicate.Title += 1;
if (syndicate.Title > 0 && manifest.favours.length != 0) {
syndicate.FreeFavorsEarned ??= [];
if (!syndicate.FreeFavorsEarned.includes(syndicate.Title)) {
syndicate.FreeFavorsEarned.push(syndicate.Title);
}
}
if (reward) {
combineInventoryChanges(
res.InventoryChanges,
(await handleStoreItemAcquisition(reward, inventory)).InventoryChanges
);
}
await inventory.save();
response.json(res);
}; };
export { syndicateSacrificeController }; interface ISyndicateSacrificeRequest {
AffiliationTag: string;
SacrificeLevel: number;
AllowMultiple: boolean;
}
interface ISyndicateSacrificeResponse {
AffiliationTag: string;
Level: number;
LevelIncrease: number;
InventoryChanges: IInventoryChanges;
NewEpisodeReward: boolean;
}

View File

@ -0,0 +1,72 @@
import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { addMiscItems, getInventory, getStandingLimit, updateStandingLimit } from "@/src/services/inventoryService";
import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
import { IOid } from "@/src/types/commonTypes";
import { ExportSyndicates } from "warframe-public-export-plus";
import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper";
export const syndicateStandingBonusController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const request = JSON.parse(String(req.body)) as ISyndicateStandingBonusRequest;
const syndicateMeta = ExportSyndicates[request.Operation.AffiliationTag];
let gainedStanding = 0;
request.Operation.Items.forEach(item => {
const medallion = (syndicateMeta.medallions ?? []).find(medallion => medallion.itemType == item.ItemType);
if (medallion) {
gainedStanding += medallion.standing * item.ItemCount;
}
item.ItemCount *= -1;
});
const inventory = await getInventory(accountId);
addMiscItems(inventory, request.Operation.Items);
let syndicate = inventory.Affiliations.find(x => x.Tag == request.Operation.AffiliationTag);
if (!syndicate) {
syndicate =
inventory.Affiliations[
inventory.Affiliations.push({ Tag: request.Operation.AffiliationTag, Standing: 0 }) - 1
];
}
const max = getMaxStanding(syndicateMeta, syndicate.Title ?? 0);
if (syndicate.Standing + gainedStanding > max) {
gainedStanding = max - syndicate.Standing;
}
if (syndicateMeta.medallionsCappedByDailyLimit) {
if (gainedStanding > getStandingLimit(inventory, syndicateMeta.dailyLimitBin)) {
gainedStanding = getStandingLimit(inventory, syndicateMeta.dailyLimitBin);
}
updateStandingLimit(inventory, syndicateMeta.dailyLimitBin, gainedStanding);
}
syndicate.Standing += gainedStanding;
await inventory.save();
res.json({
InventoryChanges: {
MiscItems: request.Operation.Items
},
AffiliationMods: [
{
Tag: request.Operation.AffiliationTag,
Standing: gainedStanding
}
]
});
};
interface ISyndicateStandingBonusRequest {
Operation: {
AffiliationTag: string;
AlternateBonusReward: ""; // ???
Items: IMiscItem[];
};
ModularWeaponId: IOid; // Seems to just be "000000000000000000000000", also note there's a "Category" query field
}

View File

@ -4,7 +4,6 @@ import { getInventory } from "@/src/services/inventoryService";
import { ITaunt } from "@/src/types/inventoryTypes/inventoryTypes"; import { ITaunt } from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const tauntHistoryController: RequestHandler = async (req, res) => { export const tauntHistoryController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);

View File

@ -4,6 +4,7 @@ import { getInventory } from "@/src/services/inventoryService";
import { IMongoDate } from "@/src/types/commonTypes"; import { IMongoDate } from "@/src/types/commonTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { unixTimesInMs } from "@/src/constants/timeConstants"; import { unixTimesInMs } from "@/src/constants/timeConstants";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
interface ITrainingResultsRequest { interface ITrainingResultsRequest {
numLevelsGained: number; numLevelsGained: number;
@ -12,20 +13,18 @@ interface ITrainingResultsRequest {
interface ITrainingResultsResponse { interface ITrainingResultsResponse {
NewTrainingDate: IMongoDate; NewTrainingDate: IMongoDate;
NewLevel: number; NewLevel: number;
InventoryChanges: any[]; InventoryChanges: IInventoryChanges;
} }
// eslint-disable-next-line @typescript-eslint/no-misused-promises
const trainingResultController: RequestHandler = async (req, res): Promise<void> => { const trainingResultController: RequestHandler = async (req, res): Promise<void> => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const trainingResults = getJSONfromString(String(req.body)) as ITrainingResultsRequest; const trainingResults = getJSONfromString<ITrainingResultsRequest>(String(req.body));
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
inventory.TrainingDate = new Date(Date.now() + unixTimesInMs.day);
if (trainingResults.numLevelsGained == 1) { if (trainingResults.numLevelsGained == 1) {
inventory.TrainingDate = new Date(Date.now() + unixTimesInMs.hour * 23);
inventory.PlayerLevel += 1; inventory.PlayerLevel += 1;
} }
@ -36,7 +35,7 @@ const trainingResultController: RequestHandler = async (req, res): Promise<void>
$date: { $numberLong: changedinventory.TrainingDate.getTime().toString() } $date: { $numberLong: changedinventory.TrainingDate.getTime().toString() }
}, },
NewLevel: trainingResults.numLevelsGained == 1 ? changedinventory.PlayerLevel : inventory.PlayerLevel, NewLevel: trainingResults.numLevelsGained == 1 ? changedinventory.PlayerLevel : inventory.PlayerLevel,
InventoryChanges: [] InventoryChanges: {}
} satisfies ITrainingResultsResponse); } satisfies ITrainingResultsResponse);
}; };

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