Compare commits

...

323 Commits

Author SHA1 Message Date
Kenya-DK
b72d334eab Merge remote-tracking branch 'origin/main' 2025-02-19 23:04:27 +01:00
Kenya-DK
9accf7a597 fix: use Object.Assign when updating 2025-02-19 23:04:21 +01:00
c9eae22312 merge upstream 2025-02-19 13:11:42 -08:00
Kenya-DK
2d97dee80c fix: better naming 2025-02-19 22:11:17 +01: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
Kenya-DK
6c6403f460 fix: Unable to save settings when accepting trade policy. 2025-02-19 20:29:50 +01: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
223 changed files with 22339 additions and 20791 deletions

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",
"@typescript-eslint/no-unnecessary-condition": "warn",
"no-case-declarations": "warn", "no-case-declarations": "warn",
"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

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

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

5
.gitignore vendored
View File

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

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,8 @@
# 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)
## 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,24 @@
@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
call npm run start
echo SpaceNinjaServer seems to have crashed.
:a
pause > nul
goto a

View File

@ -2,25 +2,30 @@
"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",
"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,
"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,
"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

2176
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.9.4",
"warframe-public-export-plus": "^0.4.4", "warframe-public-export-plus": "^0.5.30",
"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 http://209.141.38.3/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,98 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { addMods, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getRandomElement, getRandomInt, getRandomReward, IRngResult } 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: IRandomModChallenge = {
Type: challenge.fullName,
Progress: 0,
Required: getRandomInt(challenge.countRange[0], challenge.countRange[1])
};
if (Math.random() < challenge.complicationChance) {
const complicationsAsRngResults: IRngResult[] = [];
for (const complication of challenge.complications) {
complicationsAsRngResults.push({
type: complication.fullName,
itemCount: 1,
probability: complication.weight
});
}
fingerprintChallenge.Complication = getRandomReward(complicationsAsRngResults)!.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();
res.json({
NewMod: inventory.Upgrades[upgradeIndex].toJSON()
});
};
interface IActiveRandomModRequest {
ItemType: string;
}
interface IRandomModChallenge {
Type: string;
Progress: number;
Required: number;
Complication?: 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(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

@ -7,30 +7,32 @@ import { getRecipe } from "@/src/services/itemDataService";
import { IOid } from "@/src/types/commonTypes"; 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,
updateCurrencyByAccountId
} from "@/src/services/inventoryService";
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`);
// } // }
@ -39,14 +41,12 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
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 inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const currencyChanges = updateCurrency(inventory, recipe.buildPrice * -1, false);
addMiscItems(inventory, recipe.ingredients); addMiscItems(inventory, recipe.ingredients);
await inventory.save(); await inventory.save();
@ -57,6 +57,30 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
}); });
} else { } else {
logger.debug("Claiming Recipe", { recipe, pendingRecipe }); logger.debug("Claiming Recipe", { recipe, pendingRecipe });
if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") {
const inventory = await getInventory(accountId);
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);
await inventory.save();
}
}
let InventoryChanges = {}; let InventoryChanges = {};
if (recipe.consumeOnUse) { if (recipe.consumeOnUse) {
const recipeChanges = [ const recipeChanges = [
@ -75,14 +99,15 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
if (req.query.rush) { if (req.query.rush) {
InventoryChanges = { InventoryChanges = {
...InventoryChanges, ...InventoryChanges,
...(await updateCurrency(recipe.skipBuildTimePrice, true, accountId)) ...(await updateCurrencyByAccountId(recipe.skipBuildTimePrice, true, accountId))
}; };
} }
res.json({ const inventory = await getInventory(accountId);
InventoryChanges: { InventoryChanges = {
...InventoryChanges, ...InventoryChanges,
...(await addItem(accountId, recipe.resultType, recipe.num)).InventoryChanges ...(await addItem(inventory, recipe.resultType, recipe.num)).InventoryChanges
} };
}); await inventory.save();
res.json({ InventoryChanges });
} }
}; };

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

@ -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,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,49 @@
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;
const oldTreasure = parseFusionTreasure(request.oldTreasureName, -1);
const newTreasure = parseFusionTreasure(request.newTreasureName, 1);
// Swap treasures
addFusionTreasures(inventory, [oldTreasure]);
addFusionTreasures(inventory, [newTreasure]);
// 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();
res.end();
};

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;
@ -27,34 +25,5 @@ export const getGuildDojoController: RequestHandler = async (req, res) => {
await guild.save(); await guild.save();
} }
const dojo: IDojoClient = { res.json(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

@ -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,98 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { addMiscItems, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { getRandomWeightedReward2 } from "@/src/services/rngService";
import { ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express";
import { ExportRelics, ExportRewards, TRarity } from "warframe-public-export-plus";
export const getVoidProjectionRewardsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const data = getJSONfromString<IVoidProjectionRewardRequest>(String(req.body));
const response: IVoidProjectionRewardResponse = {
CurrentWave: data.CurrentWave,
ParticipantInfo: data.ParticipantInfo,
DifficultyTier: data.DifficultyTier
};
if (data.ParticipantInfo.QualifiesForReward) {
const relic = ExportRelics[data.ParticipantInfo.VoidProjection];
const weights = refinementToWeights[relic.quality];
logger.debug(`opening a relic of quality ${relic.quality}; rarity weights are`, weights);
const reward = getRandomWeightedReward2(
ExportRewards[relic.rewardManifest][0] as { type: string; itemCount: number; rarity: TRarity }[], // rarity is nullable in PE+ typings, but always present for relics
weights
)!;
logger.debug(`relic rolled`, reward);
response.ParticipantInfo.Reward = reward.type;
const inventory = await getInventory(accountId);
// Remove relic
addMiscItems(inventory, [
{
ItemType: data.ParticipantInfo.VoidProjection,
ItemCount: -1
}
]);
// Give reward
await handleStoreItemAcquisition(reward.type, inventory, reward.itemCount);
await inventory.save();
}
res.json(response);
};
const refinementToWeights = {
VPQ_BRONZE: {
COMMON: 0.76,
UNCOMMON: 0.22,
RARE: 0.02,
LEGENDARY: 0
},
VPQ_SILVER: {
COMMON: 0.7,
UNCOMMON: 0.26,
RARE: 0.04,
LEGENDARY: 0
},
VPQ_GOLD: {
COMMON: 0.6,
UNCOMMON: 0.34,
RARE: 0.06,
LEGENDARY: 0
},
VPQ_PLATINUM: {
COMMON: 0.5,
UNCOMMON: 0.4,
RARE: 0.1,
LEGENDARY: 0
}
};
interface IVoidProjectionRewardRequest {
CurrentWave: number;
ParticipantInfo: IParticipantInfo;
VoidTier: string;
DifficultyTier: number;
VoidProjectionRemovalHash: string;
}
interface IVoidProjectionRewardResponse {
CurrentWave: number;
ParticipantInfo: IParticipantInfo;
DifficultyTier: number;
}
interface IParticipantInfo {
AccountId: string;
Name: string;
ChosenRewardOwner: string;
MissionHash: string;
VoidProjection: string;
Reward: string;
QualifiesForReward: boolean;
HaveRewardResponse: boolean;
RewardsMultiplier: number;
RewardProjection: string;
HardModeReward: ITypeCount;
}

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,38 @@
import { RequestHandler } from "express";
import { isEmptyObject, parseString } from "@/src/helpers/general";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { addKeyChainItems, getInventory } from "@/src/services/inventoryService";
import { IGroup } from "@/src/types/loginTypes";
import { updateQuestStage } 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 addKeyChainItems(inventory, keyChainInfo);
if (isEmptyObject(inventoryChanges)) {
throw new Error("inventory changes was empty after getting keychain items: should not happen");
}
// items were added: update quest stage's i (item was given)
updateQuestStage(inventory, keyChainInfo, { i: true });
await inventory.save();
res.send(inventoryChanges);
//TODO: Check whether Wishlist is used to track items which should exist uniquely in the inventory
/*
some items are added or removed (not sure) to the wishlist, in that case a
WishlistChanges: ["/Lotus/Types/Items/ShipFeatureItems/ArsenalFeatureItem"],
is added to the response, need to determine for which items this is the case and what purpose this has.
*/
//{"KeyChain":"/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain","ChainStage":0}
//{"WishlistChanges":["/Lotus/Types/Items/ShipFeatureItems/ArsenalFeatureItem"],"MiscItems":[{"ItemType":"/Lotus/Types/Items/ShipFeatureItems/ArsenalFeatureItem","ItemCount":1}]}
};
export interface IKeyChainRequest {
KeyChain: string;
ChainStage: number;
Groups?: IGroup[];
}

View File

@ -0,0 +1,35 @@
import { IKeyChainRequest } from "@/src/controllers/api/giveKeyChainTriggeredItemsController";
import { IMessage } from "@/src/models/inboxModel";
import { createMessage } from "@/src/services/inboxService";
import { getInventory } from "@/src/services/inventoryService";
import { getKeyChainMessage } from "@/src/services/itemDataService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { updateQuestStage } 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 keyChainMessage = getKeyChainMessage(keyChainInfo);
const message = {
sndr: keyChainMessage.sender,
msg: keyChainMessage.body,
sub: keyChainMessage.title,
att: keyChainMessage.attachments.length > 0 ? keyChainMessage.attachments : undefined,
countedAtt: keyChainMessage.countedAttachments.length > 0 ? keyChainMessage.countedAttachments : undefined,
icon: keyChainMessage.icon ?? "",
transmission: keyChainMessage.transmission ?? "",
highPriority: keyChainMessage.highPriority ?? false,
r: false
} satisfies IMessage;
await createMessage(accountId, [message]);
const inventory = await getInventory(accountId, "QuestKeys");
updateQuestStage(inventory, keyChainInfo, { m: true });
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

@ -1,5 +1,127 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getGuildForRequestEx } from "@/src/services/guildService";
import { ExportDojoRecipes } 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";
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)) {
guild.TechProjects.push({
ItemType: data.RecipeType!,
ReqCredits: scaleRequiredCount(recipe.price),
ReqItems: recipe.ingredients.map(x => ({
ItemType: x.ItemType,
ItemCount: scaleRequiredCount(x.ItemCount)
})),
State: 0
});
}
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),
MiscItems: miscItemChanges
};
if (techProject.ReqCredits == 0 && !techProject.ReqItems.find(x => x.ItemCount > 0)) {
// This research is now fully funded.
techProject.State = 1;
const recipe = ExportDojoRecipes.research[data.RecipeType!];
techProject.CompletionDate = new Date(new Date().getTime() + recipe.time * 1000);
}
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}`);
}
};
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[];
}
const scaleRequiredCount = (count: number): number => {
// The recipes in the export are for Moon clans. For now we'll just assume we only have Ghost clans.
return Math.max(1, Math.trunc(count / 100));
}; };

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,28 @@
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,
IInfestedFoundryDatabase,
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";
// 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 +48,47 @@ 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] = {};
// remove bile
const bile = inventory.InfestedFoundry!.Resources!.find(
x => x.ItemType == "/Lotus/Types/Items/InfestedFoundry/HelminthBile"
)!;
bile.Count -= 300;
await inventory.save();
res.json({
InventoryChanges: {
MiscItems: miscItemChanges,
InfestedFoundry: inventory.toJSON().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 +103,233 @@ 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(); 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();
res.json({
InventoryChanges: {
InfestedFoundry: inventory.toJSON().InfestedFoundry
}
});
break;
}
case "a": {
// subsume warframe
const request = getJSONfromString<IHelminthSubsumeRequest>(String(req.body));
const inventory = await getInventory(accountId);
const recipe = getRecipe(request.Recipe)!;
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 && 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();
res.json({
InventoryChanges: {
Recipes: recipeChanges,
RemovedIdItems: [
{
ItemId: request.SuitId
}
],
SuitBin: {
count: -1,
platinum: 0,
Slots: 1
},
InfestedFoundry: inventory.toJSON().InfestedFoundry
}
});
break;
}
case "r": {
// rush subsume
const inventory = await getInventory(accountId);
const currencyChanges = updateCurrency(inventory, 50, true);
const recipeChanges = handleSubsumeCompletion(inventory);
await inventory.save();
res.json({
InventoryChanges: {
...currencyChanges,
Recipes: recipeChanges,
InfestedFoundry: inventory.toJSON().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);
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();
res.json({
SuitId: request.SuitId,
OffensiveUpgrade: request.OffensiveUpgradeType,
DefensiveUpgrade: request.DefensiveUpgradeType,
UpgradesExpiry: toMongoDate(upgradesExpiry),
InventoryChanges: {
Recipes: recipeChanges,
InfestedFoundry: inventory.toJSON().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 +340,166 @@ 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;
};
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,81 @@
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 { handleSubsumeCompletion } from "./infestedFoundryController";
import { allDailyAffiliationKeys } 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;
await inventory.save();
}
if (config.infiniteResources) { 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 +90,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 +118,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 +158,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 +173,58 @@ 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;
}
}
// 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 +237,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,5 +1,5 @@
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";
@ -18,19 +18,16 @@ 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);
//TODO: check which slot was purchased because pvpBonus is also possible //TODO: check which slot was purchased because pvpBonus is also possible
const currencyChanges = await updateCurrency(20, true, accountId); const inventory = await getInventory(accountId);
await updateSlots(accountId, InventorySlot.PVE_LOADOUTS, 1, 1); const currencyChanges = updateCurrency(inventory, 20, true);
updateSlots(inventory, InventorySlot.PVE_LOADOUTS, 1, 1);
//console.log({ InventoryChanges: currencyChanges }, " added loadout changes:"); await inventory.save();
res.json({ InventoryChanges: currencyChanges }); res.json({ InventoryChanges: currencyChanges });
}; };

View File

@ -1,55 +1,51 @@
/* 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 { 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 +68,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,
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: groups, Groups: groups,
platformCDNs: platformCDNs, platformCDNs: platformCDNs,
NRS: [config.myAddress], NRS: [config.myAddress],
DTLS: DTLS, DTLS: DTLS,
IRC: config.myIrcAddresses ?? [config.myAddress], IRC: config.myIrcAddresses ?? [config.myAddress],
HUB: HUB, HUB: HUB,
BuildLabel: buildConfig.buildLabel, BuildLabel: buildLabel,
MatchmakingBuildId: buildConfig.matchmakingBuildId MatchmakingBuildId: buildConfig.matchmakingBuildId
}; };
response.json(newLoginResponse);
}; };
export { loginController };

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

@ -5,7 +5,11 @@ import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
import { getInventory, updateCurrency, addEquipment, addMiscItems } from "@/src/services/inventoryService"; import { getInventory, updateCurrency, addEquipment, addMiscItems } from "@/src/services/inventoryService";
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 +27,19 @@ 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);
// Give weapon // Give weapon
const weapon = await addEquipment(category, data.WeaponType, accountId, data.Parts); const weapon = addEquipment(inventory, category, data.WeaponType, data.Parts);
// 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,7 +47,11 @@ 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();

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,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);

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,19 @@
import { getGuildForRequest } from "@/src/services/guildService"; import { getDojoClient, getGuildForRequest } from "@/src/services/guildService";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { ExportDojoRecipes } from "warframe-public-export-plus";
// 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( const component = guild.DojoComponents!.splice(
guild.DojoComponents!.findIndex(x => x._id.toString() === componentId), guild.DojoComponents!.findIndex(x => x._id.toString() === componentId),
1 1
); )[0];
const room = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == component.pf);
if (room) {
guild.DojoCapacity -= room.capacity;
guild.DojoEnergy -= room.energy;
}
await guild.save(); await guild.save();
res.json({ res.json(getDojoClient(guild, 1));
DojoRequestStatus: 1
});
}; };

View File

@ -1,9 +1,104 @@
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 { ExportUpgrades } from "warframe-public-export-plus";
import { getRandomElement } from "@/src/services/rngService";
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 upgrade = inventory.Upgrades.id(request.ItemIds[0])!;
const fingerprint = JSON.parse(upgrade.UpgradeFingerprint!) as IUnveiledRivenFingerprint;
fingerprint.rerolls ??= 0;
const kuvaCost = fingerprint.rerolls < rerollCosts.length ? rerollCosts[fingerprint.rerolls] : 3500;
addMiscItems(inventory, [
{
ItemType: "/Lotus/Types/Items/MiscItems/Kuva",
ItemCount: kuvaCost * -1
}
]);
fingerprint.rerolls++;
upgrade.UpgradeFingerprint = JSON.stringify(fingerprint);
randomiseStats(upgrade.ItemType, fingerprint);
upgrade.PendingRerollFingerprint = JSON.stringify(fingerprint);
await inventory.save();
res.json({
changes: [
{
ItemId: { $oid: request.ItemIds[0] },
UpgradeFingerprint: upgrade.UpgradeFingerprint,
PendingRerollFingerprint: upgrade.PendingRerollFingerprint
}
],
cost: kuvaCost
});
} 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 }; const randomiseStats = (randomModType: string, fingerprint: IUnveiledRivenFingerprint): void => {
const meta = ExportUpgrades[randomModType];
fingerprint.buffs = [];
const numBuffs = 2 + Math.trunc(Math.random() * 2); // 2 or 3
const buffEntries = meta.upgradeEntries!.filter(x => x.canBeBuff);
for (let i = 0; i != numBuffs; ++i) {
const buffIndex = Math.trunc(Math.random() * buffEntries.length);
const entry = buffEntries[buffIndex];
fingerprint.buffs.push({ Tag: entry.tag, Value: Math.trunc(Math.random() * 0x40000000) });
buffEntries.splice(buffIndex, 1);
}
fingerprint.curses = [];
if (Math.random() < 0.5) {
const entry = getRandomElement(
meta.upgradeEntries!.filter(x => x.canBeCurse && !fingerprint.buffs.find(y => y.Tag == x.tag))
);
fingerprint.curses.push({ Tag: entry.tag, Value: Math.trunc(Math.random() * 0x40000000) });
}
};
type RerollRandomModRequest = LetsGoGamblingRequest | AwDangitRequest;
interface LetsGoGamblingRequest {
ItemIds: string[];
}
interface AwDangitRequest {
ItemId: string;
CommitReroll: boolean;
}
interface IUnveiledRivenFingerprint {
compat: string;
lim: number;
lvl: number;
lvlReq: 0;
rerolls?: number;
pol: string;
buffs: IRivenStat[];
curses: IRivenStat[];
}
interface IRivenStat {
Tag: string;
Value: number;
}
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,51 @@ 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.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 +120,51 @@ 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[];
};
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!.find(x => x._id.equals(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(getDojoClient(guild, 1));
};
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

@ -1,18 +1,25 @@
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";
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;
const room = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == request.PlacedComponent.pf);
if (room) {
guild.DojoCapacity += room.capacity;
guild.DojoEnergy += room.energy;
}
guild.DojoComponents!.push({ guild.DojoComponents!.push({
_id: new Types.ObjectId(), _id: new Types.ObjectId(),
pf: request.PlacedComponent.pf, pf: request.PlacedComponent.pf,
@ -23,7 +30,5 @@ export const startDojoRecipeController: RequestHandler = async (req, res) => {
CompletionTime: new Date(Date.now()) // TOOD: Omit this field & handle the "Collecting Materials" state. CompletionTime: new Date(Date.now()) // TOOD: Omit this field & handle the "Collecting Materials" state.
}); });
await guild.save(); await guild.save();
res.json({ res.json(getDojoClient(guild, 0));
DojoRequestStatus: 0
});
}; };

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,106 @@
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";
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 ingredientsInverse = recipe.ingredients.map(component => ({
ItemType: component.ItemType,
ItemCount: component.ItemCount * -1
}));
const inventory = await getInventory(accountId);
updateCurrency(inventory, recipe.buildPrice, false);
addMiscItems(inventory, ingredientsInverse);
//buildtime is in seconds
const completionDate = new Date(Date.now() + recipe.buildTime * unixTimesInMs.second);
inventory.PendingRecipes.push({
ItemType: recipeName,
CompletionDate: completionDate,
_id: new Types.ObjectId()
});
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);
}
}
const newInventory = await inventory.save();
res.json({
RecipeId: { $oid: newInventory.PendingRecipes[newInventory.PendingRecipes.length - 1]._id.toString() }
});
}; };

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);
}; };

View File

@ -0,0 +1,12 @@
import { RequestHandler } from "express";
import { updateShipFeature } from "@/src/services/personalRoomsService";
import { IUnlockShipFeatureRequest } from "@/src/types/requestTypes";
import { parseString } from "@/src/helpers/general";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const unlockShipFeatureController: RequestHandler = async (req, res) => {
const accountId = parseString(req.query.accountId);
const shipFeatureRequest = JSON.parse((req.body as string).toString()) as IUnlockShipFeatureRequest;
await updateShipFeature(accountId, shipFeatureRequest.Feature);
res.send([]);
};

View File

@ -4,9 +4,8 @@ import { getAccountIdForRequest } from "@/src/services/loginService";
import { updateChallengeProgress } from "@/src/services/inventoryService"; import { updateChallengeProgress } from "@/src/services/inventoryService";
import { IUpdateChallengeProgressRequest } from "@/src/types/requestTypes"; import { IUpdateChallengeProgressRequest } from "@/src/types/requestTypes";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
const updateChallengeProgressController: RequestHandler = async (req, res) => { const updateChallengeProgressController: RequestHandler = async (req, res) => {
const payload = getJSONfromString(String(req.body)) as IUpdateChallengeProgressRequest; const payload = getJSONfromString<IUpdateChallengeProgressRequest>(String(req.body));
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
await updateChallengeProgress(payload, accountId); await updateChallengeProgress(payload, accountId);

View File

@ -0,0 +1,47 @@
import { RequestHandler } from "express";
import { parseString } from "@/src/helpers/general";
import { logger } from "@/src/utils/logger";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { updateQuestKey, IUpdateQuestRequest } from "@/src/services/questService";
import { getQuestCompletionItems } from "@/src/services/itemDataService";
import { addItems, getInventory } from "@/src/services/inventoryService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const updateQuestController: RequestHandler = async (req, res) => {
const accountId = parseString(req.query.accountId);
const updateQuestRequest = getJSONfromString<IUpdateQuestRequest>((req.body as string).toString());
// updates should be made only to one quest key per request
if (updateQuestRequest.QuestKeys.length > 1) {
throw new Error(`quest keys array should only have 1 item, but has ${updateQuestRequest.QuestKeys.length}`);
}
const inventory = await getInventory(accountId);
const updateQuestResponse: { CustomData?: string; InventoryChanges?: IInventoryChanges; MissionRewards: [] } = {
MissionRewards: []
};
updateQuestKey(inventory, updateQuestRequest.QuestKeys);
if (updateQuestRequest.QuestKeys[0].Completed) {
logger.debug(`completed quest ${updateQuestRequest.QuestKeys[0].ItemType} `);
const questKeyName = updateQuestRequest.QuestKeys[0].ItemType;
const questCompletionItems = getQuestCompletionItems(questKeyName);
logger.debug(`quest completion items`, questCompletionItems);
if (questCompletionItems) {
const inventoryChanges = await addItems(inventory, questCompletionItems);
updateQuestResponse.InventoryChanges = inventoryChanges;
}
inventory.ActiveQuest = "";
}
//TODO: might need to parse the custom data and add the associated items to inventory
if (updateQuestRequest.QuestKeys[0].CustomData) {
updateQuestResponse.CustomData = updateQuestRequest.QuestKeys[0].CustomData;
}
await inventory.save();
res.send(updateQuestResponse);
};

View File

@ -5,8 +5,8 @@ const updateSessionGetController: RequestHandler = (_req, res) => {
res.json({}); res.json({});
}; };
const updateSessionPostController: RequestHandler = (_req, res) => { const updateSessionPostController: RequestHandler = (_req, res) => {
console.log("UpdateSessions POST Request:", JSON.parse(String(_req.body))); //console.log("UpdateSessions POST Request:", JSON.parse(String(_req.body)));
console.log("ReqID:", _req.query.sessionId as string); //console.log("ReqID:", _req.query.sessionId as string);
updateSession(_req.query.sessionId as string, String(_req.body)); updateSession(_req.query.sessionId as string, String(_req.body));
res.json({}); res.json({});
}; };

View File

@ -4,13 +4,12 @@ import { updateTheme } from "@/src/services/inventoryService";
import { IThemeUpdateRequest } from "@/src/types/requestTypes"; import { IThemeUpdateRequest } from "@/src/types/requestTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
const updateThemeController: RequestHandler = async (request, response) => { const updateThemeController: RequestHandler = async (request, response) => {
const accountId = await getAccountIdForRequest(request); const accountId = await getAccountIdForRequest(request);
const body = String(request.body); const body = String(request.body);
try { try {
const json = getJSONfromString(body) as IThemeUpdateRequest; const json = getJSONfromString<IThemeUpdateRequest>(body);
if (typeof json !== "object") { if (typeof json !== "object") {
throw new Error("Invalid data format"); throw new Error("Invalid data format");
} }

View File

@ -3,24 +3,27 @@ import { IUpgradesRequest } from "@/src/types/requestTypes";
import { import {
ArtifactPolarity, ArtifactPolarity,
IEquipmentDatabase, IEquipmentDatabase,
EquipmentFeatures EquipmentFeatures,
IAbilityOverride
} from "@/src/types/inventoryTypes/commonInventoryTypes"; } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; import { IInventoryClient, IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { addMiscItems, getInventory, updateCurrency } from "@/src/services/inventoryService"; import { addMiscItems, addRecipes, getInventory, updateCurrency } from "@/src/services/inventoryService";
import { getRecipeByResult } from "@/src/services/itemDataService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { addInfestedFoundryXP } from "./infestedFoundryController";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const upgradesController: RequestHandler = async (req, res) => { export const upgradesController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const payload = JSON.parse(String(req.body)) as IUpgradesRequest; const payload = JSON.parse(String(req.body)) as IUpgradesRequest;
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const InventoryChanges: any = {}; const inventoryChanges: IInventoryChanges = {};
for (const operation of payload.Operations) { for (const operation of payload.Operations) {
if ( if (
operation.UpgradeRequirement == "/Lotus/Types/Items/MiscItems/ModSlotUnlocker" || operation.UpgradeRequirement == "/Lotus/Types/Items/MiscItems/ModSlotUnlocker" ||
operation.UpgradeRequirement == "/Lotus/Types/Items/MiscItems/CustomizationSlotUnlocker" operation.UpgradeRequirement == "/Lotus/Types/Items/MiscItems/CustomizationSlotUnlocker"
) { ) {
await updateCurrency(10, true, accountId); updateCurrency(inventory, 10, true);
} else { } else {
addMiscItems(inventory, [ addMiscItems(inventory, [
{ {
@ -30,112 +33,131 @@ export const upgradesController: RequestHandler = async (req, res) => {
]); ]);
} }
switch (operation.UpgradeRequirement) { if (operation.OperationType == "UOT_ABILITY_OVERRIDE") {
case "/Lotus/Types/Items/MiscItems/OrokinReactor": console.assert(payload.ItemCategory == "Suits");
case "/Lotus/Types/Items/MiscItems/OrokinCatalyst": const suit = inventory.Suits.id(payload.ItemId.$oid)!;
for (const item of inventory[payload.ItemCategory]) {
if (item._id.toString() == payload.ItemId.$oid) { let newAbilityOverride: IAbilityOverride | undefined;
item.Features ??= 0; let totalPercentagePointsConsumed = 0;
item.Features |= EquipmentFeatures.DOUBLE_CAPACITY; if (operation.UpgradeRequirement != "") {
break; newAbilityOverride = {
} Ability: operation.UpgradeRequirement,
Index: operation.PolarizeSlot
};
const recipe = getRecipeByResult(operation.UpgradeRequirement)!;
for (const ingredient of recipe.ingredients) {
totalPercentagePointsConsumed += ingredient.ItemCount / 10;
inventory.InfestedFoundry!.Resources!.find(x => x.ItemType == ingredient.ItemType)!.Count -=
ingredient.ItemCount;
} }
break; }
case "/Lotus/Types/Items/MiscItems/UtilityUnlocker":
case "/Lotus/Types/Items/MiscItems/WeaponUtilityUnlocker": for (const entry of operation.PolarityRemap) {
for (const item of inventory[payload.ItemCategory]) { suit.Configs[entry.Slot] ??= {};
if (item._id.toString() == payload.ItemId.$oid) { suit.Configs[entry.Slot].AbilityOverride = newAbilityOverride;
item.Features ??= 0; }
item.Features |= EquipmentFeatures.UTILITY_SLOT;
break; const recipeChanges = addInfestedFoundryXP(inventory.InfestedFoundry!, totalPercentagePointsConsumed * 8);
} addRecipes(inventory, recipeChanges);
}
break; inventoryChanges.Recipes = recipeChanges;
case "/Lotus/Types/Items/MiscItems/HeavyWeaponCatalyst": inventoryChanges.InfestedFoundry = inventory.toJSON<IInventoryClient>().InfestedFoundry;
console.assert(payload.ItemCategory == "SpaceGuns"); } else
for (const item of inventory[payload.ItemCategory]) { switch (operation.UpgradeRequirement) {
if (item._id.toString() == payload.ItemId.$oid) { case "/Lotus/Types/Items/MiscItems/OrokinReactor":
item.Features ??= 0; case "/Lotus/Types/Items/MiscItems/OrokinCatalyst":
item.Features |= EquipmentFeatures.GRAVIMAG_INSTALLED; for (const item of inventory[payload.ItemCategory]) {
break; if (item._id.toString() == payload.ItemId.$oid) {
} item.Features ??= 0;
} item.Features |= EquipmentFeatures.DOUBLE_CAPACITY;
break; break;
case "/Lotus/Types/Items/MiscItems/WeaponPrimaryArcaneUnlocker":
case "/Lotus/Types/Items/MiscItems/WeaponSecondaryArcaneUnlocker":
case "/Lotus/Types/Items/MiscItems/WeaponMeleeArcaneUnlocker":
case "/Lotus/Types/Items/MiscItems/WeaponAmpArcaneUnlocker":
for (const item of inventory[payload.ItemCategory]) {
if (item._id.toString() == payload.ItemId.$oid) {
item.Features ??= 0;
item.Features |= EquipmentFeatures.ARCANE_SLOT;
break;
}
}
break;
case "/Lotus/Types/Items/MiscItems/Forma":
case "/Lotus/Types/Items/MiscItems/FormaUmbra":
case "/Lotus/Types/Items/MiscItems/FormaAura":
case "/Lotus/Types/Items/MiscItems/FormaStance":
for (const item of inventory[payload.ItemCategory]) {
if (item._id.toString() == payload.ItemId.$oid) {
item.XP = 0;
setSlotPolarity(item, operation.PolarizeSlot, operation.PolarizeValue);
item.Polarized ??= 0;
item.Polarized += 1;
break;
}
}
break;
case "/Lotus/Types/Items/MiscItems/ModSlotUnlocker":
for (const item of inventory[payload.ItemCategory]) {
if (item._id.toString() == payload.ItemId.$oid) {
item.ModSlotPurchases ??= 0;
item.ModSlotPurchases += 1;
InventoryChanges[payload.ItemCategory] = {
ItemId: {
$oid: payload.ItemId.$oid
},
ModSlotPurchases: item.ModSlotPurchases
};
break;
}
}
break;
case "/Lotus/Types/Items/MiscItems/CustomizationSlotUnlocker":
for (const item of inventory[payload.ItemCategory]) {
if (item._id.toString() == payload.ItemId.$oid) {
item.CustomizationSlotPurchases ??= 0;
item.CustomizationSlotPurchases += 1;
InventoryChanges[payload.ItemCategory] = {
ItemId: {
$oid: payload.ItemId.$oid
},
CustomizationSlotPurchases: item.CustomizationSlotPurchases
};
break;
}
}
break;
case "":
console.assert(operation.OperationType == "UOT_SWAP_POLARITY");
for (const item of inventory[payload.ItemCategory]) {
if (item._id.toString() == payload.ItemId.$oid) {
for (let i = 0; i != operation.PolarityRemap.length; ++i) {
if (operation.PolarityRemap[i].Slot != i) {
setSlotPolarity(item, i, operation.PolarityRemap[i].Value);
}
} }
break;
} }
} break;
break; case "/Lotus/Types/Items/MiscItems/UtilityUnlocker":
default: case "/Lotus/Types/Items/MiscItems/WeaponUtilityUnlocker":
throw new Error("Unsupported upgrade: " + operation.UpgradeRequirement); for (const item of inventory[payload.ItemCategory]) {
} if (item._id.toString() == payload.ItemId.$oid) {
item.Features ??= 0;
item.Features |= EquipmentFeatures.UTILITY_SLOT;
break;
}
}
break;
case "/Lotus/Types/Items/MiscItems/HeavyWeaponCatalyst":
console.assert(payload.ItemCategory == "SpaceGuns");
for (const item of inventory[payload.ItemCategory]) {
if (item._id.toString() == payload.ItemId.$oid) {
item.Features ??= 0;
item.Features |= EquipmentFeatures.GRAVIMAG_INSTALLED;
break;
}
}
break;
case "/Lotus/Types/Items/MiscItems/WeaponPrimaryArcaneUnlocker":
case "/Lotus/Types/Items/MiscItems/WeaponSecondaryArcaneUnlocker":
case "/Lotus/Types/Items/MiscItems/WeaponMeleeArcaneUnlocker":
case "/Lotus/Types/Items/MiscItems/WeaponAmpArcaneUnlocker":
for (const item of inventory[payload.ItemCategory]) {
if (item._id.toString() == payload.ItemId.$oid) {
item.Features ??= 0;
item.Features |= EquipmentFeatures.ARCANE_SLOT;
break;
}
}
break;
case "/Lotus/Types/Items/MiscItems/Forma":
case "/Lotus/Types/Items/MiscItems/FormaUmbra":
case "/Lotus/Types/Items/MiscItems/FormaAura":
case "/Lotus/Types/Items/MiscItems/FormaStance":
for (const item of inventory[payload.ItemCategory]) {
if (item._id.toString() == payload.ItemId.$oid) {
item.XP = 0;
setSlotPolarity(item, operation.PolarizeSlot, operation.PolarizeValue);
item.Polarized ??= 0;
item.Polarized += 1;
break;
}
}
break;
case "/Lotus/Types/Items/MiscItems/ModSlotUnlocker":
for (const item of inventory[payload.ItemCategory]) {
if (item._id.toString() == payload.ItemId.$oid) {
item.ModSlotPurchases ??= 0;
item.ModSlotPurchases += 1;
break;
}
}
break;
case "/Lotus/Types/Items/MiscItems/CustomizationSlotUnlocker":
for (const item of inventory[payload.ItemCategory]) {
if (item._id.toString() == payload.ItemId.$oid) {
item.CustomizationSlotPurchases ??= 0;
item.CustomizationSlotPurchases += 1;
break;
}
}
break;
case "":
console.assert(operation.OperationType == "UOT_SWAP_POLARITY");
for (const item of inventory[payload.ItemCategory]) {
if (item._id.toString() == payload.ItemId.$oid) {
for (let i = 0; i != operation.PolarityRemap.length; ++i) {
if (operation.PolarityRemap[i].Slot != i) {
setSlotPolarity(item, i, operation.PolarityRemap[i].Value);
}
}
break;
}
}
break;
default:
throw new Error("Unsupported upgrade: " + operation.UpgradeRequirement);
}
} }
await inventory.save(); await inventory.save();
res.json({ InventoryChanges }); res.json({ InventoryChanges: inventoryChanges });
}; };
const setSlotPolarity = (item: IEquipmentDatabase, slot: number, polarity: ArtifactPolarity): void => { const setSlotPolarity = (item: IEquipmentDatabase, slot: number, polarity: ArtifactPolarity): void => {

View File

@ -0,0 +1,17 @@
import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory } from "@/src/services/inventoryService";
export const addCurrencyController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const request = req.body as IAddCurrencyRequest;
inventory[request.currency] += request.delta;
await inventory.save();
res.end();
};
interface IAddCurrencyRequest {
currency: "RegularCredits" | "PremiumCredits" | "FusionPoints" | "PrimeTokens";
delta: number;
}

View File

@ -1,28 +0,0 @@
import { getAccountIdForRequest } from "@/src/services/loginService";
import { ItemType, toAddItemRequest } from "@/src/helpers/customHelpers/addItemHelpers";
import { getWeaponType } from "@/src/services/itemDataService";
import { addPowerSuit, addEquipment } from "@/src/services/inventoryService";
import { RequestHandler } from "express";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
const addItemController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const request = toAddItemRequest(req.body);
switch (request.type) {
case ItemType.Powersuit:
const powersuit = await addPowerSuit(request.InternalName, accountId);
res.json(powersuit);
return;
case ItemType.Weapon:
const weaponType = getWeaponType(request.InternalName);
const weapon = await addEquipment(weaponType, request.InternalName, accountId);
res.json(weapon);
break;
default:
res.status(400).json({ error: "something went wrong" });
break;
}
};
export { addItemController };

View File

@ -0,0 +1,19 @@
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory, addItem } from "@/src/services/inventoryService";
import { RequestHandler } from "express";
export const addItemsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const requests = req.body as IAddItemRequest[];
const inventory = await getInventory(accountId);
for (const request of requests) {
await addItem(inventory, request.ItemType, request.ItemCount);
}
await inventory.save();
res.end();
};
interface IAddItemRequest {
ItemType: string;
ItemCount: number;
}

View File

@ -0,0 +1,31 @@
import { addGearExpByCategory, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express";
import { ExportMisc } from "warframe-public-export-plus";
export const addXpController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const request = req.body as IAddXpRequest;
for (const [category, gear] of Object.entries(request)) {
for (const clientItem of gear) {
const dbItem = inventory[category as TEquipmentKey].id(clientItem.ItemId.$oid);
if (dbItem) {
if (dbItem.ItemType in ExportMisc.uniqueLevelCaps) {
if ((dbItem.Polarized ?? 0) < 5) {
dbItem.Polarized = 5;
}
}
}
}
addGearExpByCategory(inventory, gear, category as TEquipmentKey);
}
await inventory.save();
res.end();
};
type IAddXpRequest = {
[_ in TEquipmentKey]: IEquipmentClient[];
};

View File

@ -1,15 +1,16 @@
import { toCreateAccount, toDatabaseAccount } from "@/src/helpers/customHelpers/customHelpers"; import { toCreateAccount, toDatabaseAccount } from "@/src/helpers/customHelpers/customHelpers";
import { createAccount } from "@/src/services/loginService"; import { createAccount, isNameTaken } from "@/src/services/loginService";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
const createAccountController: RequestHandler = async (req, res) => { const createAccountController: RequestHandler = async (req, res) => {
const createAccountData = toCreateAccount(req.body); const createAccountData = toCreateAccount(req.body);
const databaseAccount = toDatabaseAccount(createAccountData); if (await isNameTaken(createAccountData.DisplayName)) {
res.status(409).json("Name already in use");
const account = await createAccount(databaseAccount); } else {
const databaseAccount = toDatabaseAccount(createAccountData);
res.json(account); const account = await createAccount(databaseAccount);
res.json(account);
}
}; };
export { createAccountController }; export { createAccountController };

View File

@ -0,0 +1,14 @@
import { createMessage, IMessageCreationTemplate } from "@/src/services/inboxService";
import { RequestHandler } from "express";
export const createMessageController: RequestHandler = async (req, res) => {
const message = req.body as (IMessageCreationTemplate & { ownerId: string })[] | undefined;
if (!message) {
res.status(400).send("No message provided");
return;
}
const savedMessages = await createMessage(message[0].ownerId, message);
res.json(savedMessages);
};

View File

@ -0,0 +1,23 @@
import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { Account } from "@/src/models/loginModel";
import { Inbox } from "@/src/models/inboxModel";
import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
import { Loadout } from "@/src/models/inventoryModels/loadoutModel";
import { PersonalRooms } from "@/src/models/personalRoomsModel";
import { Ship } from "@/src/models/shipModel";
import { Stats } from "@/src/models/statsModel";
export const deleteAccountController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
await Promise.all([
Account.deleteOne({ _id: accountId }),
Inbox.deleteMany({ ownerId: accountId }),
Inventory.deleteOne({ accountOwnerId: accountId }),
Loadout.deleteOne({ loadoutOwnerId: accountId }),
PersonalRooms.deleteOne({ personalRoomsOwnerId: accountId }),
Ship.deleteOne({ ShipOwnerId: accountId }),
Stats.deleteOne({ accountOwnerId: accountId })
]);
res.end();
};

View File

@ -1,8 +1,14 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { config } from "@/src/services/configService"; import { config } from "@/src/services/configService";
import { getAccountForRequest, isAdministrator } from "@/src/services/loginService";
const getConfigDataController: RequestHandler = (_req, res) => { const getConfigDataController: RequestHandler = async (req, res) => {
res.json(config); const account = await getAccountForRequest(req);
if (isAdministrator(account)) {
res.json(config);
} else {
res.status(401).end();
}
}; };
export { getConfigDataController }; export { getConfigDataController };

View File

@ -1,9 +1,14 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getEnglishString } from "@/src/services/itemDataService"; import { getDict, getItemName, getString } from "@/src/services/itemDataService";
import { import {
ExportArcanes, ExportArcanes,
ExportAvionics,
ExportGear, ExportGear,
ExportMisc,
ExportRecipes,
ExportResources, ExportResources,
ExportSentinels,
ExportSyndicates,
ExportUpgrades, ExportUpgrades,
ExportWarframes, ExportWarframes,
ExportWeapons ExportWeapons
@ -14,75 +19,161 @@ interface ListedItem {
uniqueName: string; uniqueName: string;
name: string; name: string;
fusionLimit?: number; fusionLimit?: number;
exalted?: string[];
} }
const getItemListsController: RequestHandler = (_req, res) => { const getItemListsController: RequestHandler = (req, response) => {
const weapons = []; const lang = getDict(typeof req.query.lang == "string" ? req.query.lang : "en");
const miscitems = []; const res: Record<string, ListedItem[]> = {};
res.Suits = [];
res.LongGuns = [];
res.Melee = [];
res.ModularParts = [];
res.Pistols = [];
res.Sentinels = [];
res.SentinelWeapons = [];
res.SpaceGuns = [];
res.SpaceMelee = [];
res.SpaceSuits = [];
res.MechSuits = [];
res.miscitems = [];
res.Syndicates = [];
for (const [uniqueName, item] of Object.entries(ExportWarframes)) {
if (
item.productCategory == "Suits" ||
item.productCategory == "SpaceSuits" ||
item.productCategory == "MechSuits"
) {
res[item.productCategory].push({
uniqueName,
name: getString(item.name, lang),
exalted: item.exalted
});
}
}
for (const [uniqueName, item] of Object.entries(ExportSentinels)) {
if (item.productCategory == "Sentinels") {
res[item.productCategory].push({
uniqueName,
name: getString(item.name, lang)
});
}
}
for (const [uniqueName, item] of Object.entries(ExportWeapons)) { for (const [uniqueName, item] of Object.entries(ExportWeapons)) {
if (item.productCategory !== "OperatorAmps") { if (
if (item.totalDamage !== 0) { uniqueName.split("/")[4] == "OperatorAmplifiers" ||
weapons.push({ uniqueName.split("/")[5] == "SUModularSecondarySet1" ||
uniqueName, uniqueName.split("/")[5] == "SUModularPrimarySet1" ||
name: getEnglishString(item.name) uniqueName.split("/")[5] == "InfKitGun" ||
}); uniqueName.split("/")[5] == "HoverboardParts"
} else if (!item.excludeFromCodex) { ) {
miscitems.push({ res.ModularParts.push({
uniqueName,
name: getString(item.name, lang)
});
if (uniqueName.split("/")[5] != "SentTrainingAmplifier") {
res.miscitems.push({
uniqueName: "MiscItems:" + uniqueName, uniqueName: "MiscItems:" + uniqueName,
name: getEnglishString(item.name) name: getString(item.name, lang)
});
}
} else if (item.totalDamage !== 0) {
if (
item.productCategory == "LongGuns" ||
item.productCategory == "Pistols" ||
item.productCategory == "Melee" ||
item.productCategory == "SpaceGuns" ||
item.productCategory == "SpaceMelee" ||
item.productCategory == "SentinelWeapons"
) {
res[item.productCategory].push({
uniqueName,
name: getString(item.name, lang)
});
}
} else if (!item.excludeFromCodex) {
res.miscitems.push({
uniqueName: "MiscItems:" + uniqueName,
name: getString(item.name, lang)
});
}
}
for (const [uniqueName, item] of Object.entries(ExportResources)) {
let name = getString(item.name, lang);
if ("dissectionParts" in item) {
name = getString("/Lotus/Language/Fish/FishDisplayName", lang).split("|FISH_NAME|").join(name);
if (uniqueName.indexOf("Large") != -1) {
name = name.split("|FISH_SIZE|").join(getString("/Lotus/Language/Fish/FishSizeLargeAbbrev", lang));
} else if (uniqueName.indexOf("Medium") != -1) {
name = name.split("|FISH_SIZE|").join(getString("/Lotus/Language/Fish/FishSizeMediumAbbrev", lang));
} else {
name = name.split("|FISH_SIZE|").join(getString("/Lotus/Language/Fish/FishSizeSmallAbbrev", lang));
}
}
res.miscitems.push({
uniqueName: item.productCategory + ":" + uniqueName,
name: name
});
}
for (const [uniqueName, item] of Object.entries(ExportGear)) {
res.miscitems.push({
uniqueName: "Consumables:" + uniqueName,
name: getString(item.name, lang)
});
}
const recipeNameTemplate = getString("/Lotus/Language/Items/BlueprintAndItem", lang);
for (const [uniqueName, item] of Object.entries(ExportRecipes)) {
if (!item.hidden) {
const resultName = getItemName(item.resultType);
if (resultName) {
res.miscitems.push({
uniqueName: "Recipes:" + uniqueName,
name: recipeNameTemplate.replace("|ITEM|", getString(resultName, lang))
}); });
} }
} }
} }
for (const [uniqueName, item] of Object.entries(ExportResources)) {
miscitems.push({
uniqueName: "MiscItems:" + uniqueName,
name: getEnglishString(item.name)
});
}
for (const [uniqueName, item] of Object.entries(ExportGear)) {
miscitems.push({
uniqueName: "Consumables:" + uniqueName,
name: getEnglishString(item.name)
});
}
const mods: ListedItem[] = []; res.mods = [];
const badItems: Record<string, boolean> = {}; const badItems: Record<string, boolean> = {};
for (const [uniqueName, upgrade] of Object.entries(ExportUpgrades)) { for (const [uniqueName, upgrade] of Object.entries(ExportUpgrades)) {
mods.push({ res.mods.push({
uniqueName, uniqueName,
name: getEnglishString(upgrade.name), name: getString(upgrade.name, lang),
fusionLimit: upgrade.fusionLimit fusionLimit: upgrade.fusionLimit
}); });
if (upgrade.isStarter || upgrade.isFrivolous || upgrade.upgradeEntries) { if (upgrade.isStarter || upgrade.isFrivolous || upgrade.upgradeEntries) {
badItems[uniqueName] = true; badItems[uniqueName] = true;
} }
} }
for (const [uniqueName, arcane] of Object.entries(ExportArcanes)) { for (const [uniqueName, upgrade] of Object.entries(ExportAvionics)) {
mods.push({ res.mods.push({
uniqueName, uniqueName,
name: getEnglishString(arcane.name) name: getString(upgrade.name, lang),
fusionLimit: upgrade.fusionLimit
});
}
for (const [uniqueName, arcane] of Object.entries(ExportArcanes)) {
res.mods.push({
uniqueName,
name: getString(arcane.name, lang)
}); });
if (arcane.isFrivolous) { if (arcane.isFrivolous) {
badItems[uniqueName] = true; badItems[uniqueName] = true;
} }
} }
for (const [uniqueName, syndicate] of Object.entries(ExportSyndicates)) {
res.Syndicates.push({
uniqueName,
name: getString(syndicate.name, lang)
});
}
res.json({ response.json({
warframes: Object.entries(ExportWarframes)
.filter(([_uniqueName, warframe]) => warframe.productCategory == "Suits")
.map(([uniqueName, warframe]) => {
return {
uniqueName,
name: getEnglishString(warframe.name)
};
}),
weapons,
miscitems,
mods,
badItems, badItems,
archonCrystalUpgrades archonCrystalUpgrades,
uniqueLevelCaps: ExportMisc.uniqueLevelCaps,
...res
}); });
}; };

View File

@ -0,0 +1,27 @@
import { importInventory, importLoadOutPresets } from "@/src/services/importService";
import { getInventory } from "@/src/services/inventoryService";
import { getLoadout } from "@/src/services/loadoutService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IInventoryClient } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express";
export const importController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const request = req.body as IImportRequest;
const inventory = await getInventory(accountId);
importInventory(inventory, request.inventory);
await inventory.save();
if (request.inventory.LoadOutPresets) {
const loadout = await getLoadout(accountId);
importLoadOutPresets(loadout, request.inventory.LoadOutPresets);
await loadout.save();
}
res.end();
};
interface IImportRequest {
inventory: Partial<IInventoryClient>;
}

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