Compare commits

...

336 Commits
main ... main

Author SHA1 Message Date
3194a693b3 fix: hardcode rotation A for non-endless railjack missions (#2613)
Closes #2612

Reviewed-on: OpenWF/SpaceNinjaServer#2613
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-08-13 07:13:41 -07:00
261dbd5fdf feat: railjack abandoned caches (#2611)
Closes #2602

Reviewed-on: OpenWF/SpaceNinjaServer#2611
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: VampireKitten <dynamightkobold@gmail.com>
Co-committed-by: VampireKitten <dynamightkobold@gmail.com>
2025-08-13 07:13:26 -07:00
fd2ec696a0 feat: tactical alerts (#2607)
Includes all `Tactical Alerts` since Star Chart 3.0 with exception:
`Snowday Showdown`
`Wolf Hunt (2019)` (couldn't find corresponded `EventNode` for that)
`Void Corruption` (that's goes into `Alerts`)
All `Warframe's Anniversary`

Re #1103

Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Reviewed-on: OpenWF/SpaceNinjaServer#2607
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-08-13 07:13:05 -07:00
9cc0c76ef5 chore(webui): omit conclave from supported syndicates (#2608)
Reviewed-on: OpenWF/SpaceNinjaServer#2608
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-08-12 06:32:41 -07:00
2a4488d1dd chore(webui): update fr (#2609)
Reviewed-on: OpenWF/SpaceNinjaServer#2609
Co-authored-by: Vitruvio <vitruvio@noreply.localhost>
Co-committed-by: Vitruvio <vitruvio@noreply.localhost>
2025-08-12 06:32:27 -07:00
2e1326cde8 chore: update PE+ (#2606)
Reviewed-on: OpenWF/SpaceNinjaServer#2606
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-08-11 08:09:30 -07:00
70be467cbf feat: disruption rewards (#2605)
Closes #2599

Reviewed-on: OpenWF/SpaceNinjaServer#2605
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-08-11 08:09:15 -07:00
fac3ec01c6 chore: improve structuring of mission response types (#2604)
Reviewed-on: OpenWF/SpaceNinjaServer#2604
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-08-11 08:09:08 -07:00
ebdca760e6 chore: simplify syncing of challenge 'Completed' field (#2603)
Challenges are mostly client-authoritative, so narrow the special-casing to "challengeRewards".

Reviewed-on: OpenWF/SpaceNinjaServer#2603
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-08-11 08:08:47 -07:00
51c0ddda38 feat(goals): cetus events (#2598)
Includes `Plague Star` and `Ghoul Purge`.
Translation for webUI taken from game files.
Re #1103

Reviewed-on: OpenWF/SpaceNinjaServer#2598
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-08-11 08:08:40 -07:00
9129bdb5fc chore(webui): update zh (#2601)
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Reviewed-on: OpenWF/SpaceNinjaServer#2601
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: qingchun <qingchun@noreply.localhost>
Co-committed-by: qingchun <qingchun@noreply.localhost>
2025-08-10 16:53:03 -07:00
a4922d4c35 chore: improve handling of RJ interstitial missionInventoryUpdate (#2600)
InventoryJson should only be returned when going back to dojo, in which case RJ is also not present in the request anymore.

Reviewed-on: OpenWF/SpaceNinjaServer#2600
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-08-09 03:38:10 -07:00
679752633a feat: recover nightwave challenges (#2593)
Closes #1534

Reviewed-on: OpenWF/SpaceNinjaServer#2593
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-08-08 04:21:18 -07:00
67b5890f39 feat(webui): ukrainian translation by LoseFace (#2596)
Reviewed-on: OpenWF/SpaceNinjaServer#2596
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-08-07 12:31:38 -07:00
5d54e79e5d chore(webui): russian translation update by LoseFace (#2595)
Reviewed-on: OpenWF/SpaceNinjaServer#2595
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-08-07 12:31:28 -07:00
4606f28a58 fix(webui): incorect values for ability override request (#2591)
Reviewed-on: OpenWF/SpaceNinjaServer#2591
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-08-07 03:59:25 -07:00
a2d383ee3c fix: ignore rewardQualifications for non-endless mission types (#2590)
Closes #2586

Reviewed-on: OpenWF/SpaceNinjaServer#2590
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-08-06 04:01:01 -07:00
834b7a8196 fix(webui): email address not being lowercased (#2589)
Regression introduced by 2fa6dcc7edb34c9382c31739d8b61a84803d69c2, which threw out the change introduced by 1fd801403fc8d24851e46477258759d0149eb76f.

Reviewed-on: OpenWF/SpaceNinjaServer#2589
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-08-06 04:00:43 -07:00
4a2d863c9c chore(webui): update to Spanish translation (#2588)
Reviewed-on: OpenWF/SpaceNinjaServer#2588
Co-authored-by: hxedcl <hxedcl@noreply.localhost>
Co-committed-by: hxedcl <hxedcl@noreply.localhost>
2025-08-05 10:18:36 -07:00
9f0cd91105 chore(webui): update German translation (#2587)
I need to double check some other time if it's also called "TennoLive" in German wf, once I'm home again. Should be prob good enough for now...

Reviewed-on: OpenWF/SpaceNinjaServer#2587
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-08-05 09:47:25 -07:00
ebfef52fb1 fix(webui): proper PvPVariant check (#2585)
Closes #2584

Reviewed-on: OpenWF/SpaceNinjaServer#2585
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-08-04 05:18:45 -07:00
dd7bacd22e chore: update PE+ (#2583)
Reviewed-on: OpenWF/SpaceNinjaServer#2583
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-08-03 15:37:06 -07:00
c00967931e fix(webui): add k-drive (#2581)
Closes #2580

Reviewed-on: OpenWF/SpaceNinjaServer#2581
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-08-02 04:57:16 -07:00
b15a635e11 fix(webui): exclude zaw strike pvp variants (#2579)
Reviewed-on: OpenWF/SpaceNinjaServer#2579
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-08-02 04:57:08 -07:00
7e618539fa fix(webui): explicitly specify websocket protocol (#2578)
apparently some browsers (e.g. 2 year old chrome) need this to establish a connection. can't hurt, anyway.

Reviewed-on: OpenWF/SpaceNinjaServer#2578
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-08-01 03:47:14 -07:00
a29398fae6 chore(webui): update Chinese translation (#2577)
Reviewed-on: OpenWF/SpaceNinjaServer#2577
Co-authored-by: Corvus <corvus@noreply.localhost>
Co-committed-by: Corvus <corvus@noreply.localhost>
2025-07-31 07:46:01 -07:00
601091f1c0 chore(webui): clarify that eidolon override also takes effect on deimos (#2576)
This is an update in the English translation only.

Reviewed-on: OpenWF/SpaceNinjaServer#2576
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-31 02:28:03 -07:00
f561884f2c feat: worldState.baroTennoConRelay config (#2574)
Closes #2531

Reviewed-on: OpenWF/SpaceNinjaServer#2574
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-31 02:27:38 -07:00
6e1cb0c9f9 chore(webui): update Chinese translation (#2575)
Reviewed-on: OpenWF/SpaceNinjaServer#2575
Co-authored-by: Corvus <corvus@noreply.localhost>
Co-committed-by: Corvus <corvus@noreply.localhost>
2025-07-31 02:27:26 -07:00
9286627668 feat: star days rotation (#2573)
Closes #2567

Reviewed-on: OpenWF/SpaceNinjaServer#2573
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-30 05:44:42 -07:00
f94f2005d3 fix: script error with baroFullyStocked 2025-07-30 13:45:00 +02:00
9901b7af54 chore: rename config.json.example to config-vanilla.json (#2570)
Changing file extensions can be a bit of a chore on stock Windows, so this should simplify matters. Another bonus is that the "vanilla" clarifies the general guideline for how the defaults are configured.

Reviewed-on: OpenWF/SpaceNinjaServer#2570
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-30 04:01:02 -07:00
2fa846f465 chore(webui): update Chinese translation (#2572)
Reviewed-on: OpenWF/SpaceNinjaServer#2572
Co-authored-by: Corvus <corvus@noreply.localhost>
Co-committed-by: Corvus <corvus@noreply.localhost>
2025-07-30 03:51:42 -07:00
541ec3d702 feat: claiming of tennolive relay's secret (#2569)
Reviewed-on: OpenWF/SpaceNinjaServer#2569
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-30 01:51:07 -07:00
0a28eab65d feat: worldState.tennoLiveRelay config (#2568)
Re #2531

Reviewed-on: OpenWF/SpaceNinjaServer#2568
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-30 01:50:43 -07:00
8e639a16bd feat: initial protovyre/evolving cosmetics (#2566)
Basic handling of sending the challenge rewards to the inbox upon completion.

Re #2485. Still missing handling for the Protovyre armor pieces which require killing sentients.

Reviewed-on: OpenWF/SpaceNinjaServer#2566
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-30 01:50:23 -07:00
522924a823 chore: remove empty ModularParts arrays from equipment (#2565)
Reviewed-on: OpenWF/SpaceNinjaServer#2565
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-30 01:49:55 -07:00
48e3f324e2 chore: log when worldState time is behind real time + make sure client knows fissures are active (#2562)
Reviewed-on: OpenWF/SpaceNinjaServer#2562
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-30 01:49:34 -07:00
8f77c722cb chore(webui): update German translation (#2564)
Reviewed-on: OpenWF/SpaceNinjaServer#2564
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-07-29 07:34:04 -07:00
e7287933b5 chore(webui): update Chinese translation (#2563)
Reviewed-on: OpenWF/SpaceNinjaServer#2563
Co-authored-by: Corvus <corvus@noreply.localhost>
Co-committed-by: Corvus <corvus@noreply.localhost>
2025-07-29 07:33:57 -07:00
b21bca7a6d fix: AffiliationChanges disapears from EOM screen when bounty stage is completed (#2560)
Reviewed-on: OpenWF/SpaceNinjaServer#2560
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-07-29 00:31:48 -07:00
d30d450311 chore: add rewards for NewbieJob (#2559)
Closes #2536

Reviewed-on: OpenWF/SpaceNinjaServer#2559
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-07-29 00:31:37 -07:00
b62e326920 feat(webui): ability overrides (#2558)
Closes #851

Reviewed-on: OpenWF/SpaceNinjaServer#2558
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-07-29 00:31:29 -07:00
8b4bc114f6 chore: add logging for bounty medallion rewards (#2557)
Reviewed-on: OpenWF/SpaceNinjaServer#2557
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-07-29 00:31:19 -07:00
564aa06762 fix: correctly apply riven cipher (#2554)
The completeRandomModChallenge endpoint is only supposed to complete the challenge, what a shocker. Because we directly set a unveiled fingerprint, the game was not showing the expected UI.

Reviewed-on: OpenWF/SpaceNinjaServer#2554
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-27 06:29:12 -07:00
2e84f71af8 chore: faithful handling when ki'teer signa was rolled (#2553)
Reviewed-on: OpenWF/SpaceNinjaServer#2553
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-27 06:29:02 -07:00
ddfa98e0b2 chore: update baro.json (#2550)
Co-authored-by: BanLanGen <banlangen@noreply.localhost>
Reviewed-on: OpenWF/SpaceNinjaServer#2550
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-27 06:28:51 -07:00
bb3c3e01b0 chore: add GEAR loadout slot (#2545)
added missing GEAR loadout slot (was causing issues with saving loadout in U29)

Reviewed-on: OpenWF/SpaceNinjaServer#2545
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: azdful <mischzaripov@yandex.ru>
Co-committed-by: azdful <mischzaripov@yandex.ru>
2025-07-25 01:51:13 -07:00
695dcf98e0 chore: handle sale of fusion treasures (#2542)
Closes #2541

Reviewed-on: OpenWF/SpaceNinjaServer#2542
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-24 05:30:55 -07:00
509f7f0d9b feat: selling for Dirac (CrewShipFusionPoints) (#2540)
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Reviewed-on: OpenWF/SpaceNinjaServer#2540
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: azdful <mischzaripov@yandex.ru>
Co-committed-by: azdful <mischzaripov@yandex.ru>
2025-07-23 11:09:44 -07:00
aada031a80 chore: update mongoose (#2539)
The transform hook signature was changed in the typings, so I just updated them to be explicit about what we expect.

Reviewed-on: OpenWF/SpaceNinjaServer#2539
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-23 07:51:51 -07:00
a2a441ecb0 fix: getUsernameFromEmail returning wrong value (#2538)
Closes #2537

Reviewed-on: OpenWF/SpaceNinjaServer#2538
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-23 07:51:22 -07:00
c0a0463a68 feat: vista suite backdrop and soundscape customisation (#2534)
Closes #2532

Reviewed-on: OpenWF/SpaceNinjaServer#2534
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-22 07:34:46 -07:00
2307a40833 chore(webui): debounce inventory bulk actions (#2533)
Closes #2513

Reviewed-on: OpenWF/SpaceNinjaServer#2533
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-22 07:34:40 -07:00
304af514e2 fix(webui): handle name already being taken (#2530)
Closes #2528

Reviewed-on: OpenWF/SpaceNinjaServer#2530
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-22 07:34:31 -07:00
ddf3cd49b5 chore: handle new T value for orowyrm chest (#2527)
Closes #2526

Reviewed-on: OpenWF/SpaceNinjaServer#2527
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-22 07:34:22 -07:00
41e3f0136f chore(webui): improve auth state management 2025-07-21 20:28:19 +02:00
c0ca9d9398 fix: add try/catch around websocket message event handler (#2529)
Re #2528

Reviewed-on: OpenWF/SpaceNinjaServer#2529
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-21 07:44:54 -07:00
0f6b55beed chore(webui): update fr (#2525)
Reviewed-on: OpenWF/SpaceNinjaServer#2525
Co-authored-by: Vitruvio <vitruvio@noreply.localhost>
Co-committed-by: Vitruvio <vitruvio@noreply.localhost>
2025-07-21 03:23:12 -07:00
f8550e9afe fix: incorect deimos bounty medallion reward (#2524)
Reviewed-on: OpenWF/SpaceNinjaServer#2524
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-07-21 03:23:05 -07:00
b53c4d9125 feat: reset obstacle course (#2523)
Closes #2520

Reviewed-on: OpenWF/SpaceNinjaServer#2523
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-21 03:22:59 -07:00
922b65cfab chore: print build date when started via docker (#2517)
Docker updates can be a bit confusing so this should help users know if they're up-to-date.

Reviewed-on: OpenWF/SpaceNinjaServer#2517
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-21 03:22:46 -07:00
2f642df20a feat: reset decorations (#2516)
Closes #2514

Reviewed-on: OpenWF/SpaceNinjaServer#2516
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-21 03:22:35 -07:00
62314e89c7 fix: refund personal decos when destroying dojo room (#2522)
Closes #2521

Reviewed-on: OpenWF/SpaceNinjaServer#2522
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-20 10:35:14 -07:00
56aa3e3331 fix: placing decorations in apartment in newer game versions (#2515)
Newer game versions use BootLocation instead of IsApartment

Reviewed-on: OpenWF/SpaceNinjaServer#2515
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-20 01:29:08 -07:00
c3f486488f chore: npm update (#2512)
There were some low severity vulnerabilites audit was complaining about

Reviewed-on: OpenWF/SpaceNinjaServer#2512
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-20 00:28:46 -07:00
49c353d895 chore: disable DTLS (#2511)
Reviewed-on: OpenWF/SpaceNinjaServer#2511
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-20 00:28:37 -07:00
90ab560620 chore(webui): don't refresh inventory for sell on the tab that issued it (#2506)
Reviewed-on: OpenWF/SpaceNinjaServer#2506
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-18 15:36:10 -07:00
b0e80fcfa8 chore(webui): update German translation (#2507)
Reviewed-on: OpenWF/SpaceNinjaServer#2507
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-07-17 06:56:23 -07:00
2c62fb3c3c chore: move syncConfigWithDatabase to configService (#2505)
This avoids a cyclic dependency due to configController using this

Reviewed-on: OpenWF/SpaceNinjaServer#2505
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-17 05:04:30 -07:00
5b215733aa chore(webui): update to Spanish translation (#2503)
Reviewed-on: OpenWF/SpaceNinjaServer#2503
Co-authored-by: hxedcl <hxedcl@noreply.localhost>
Co-committed-by: hxedcl <hxedcl@noreply.localhost>
2025-07-16 23:04:26 -07:00
39866b9a2b fix: hide edit suit invigorations card on detailed view load (#2500)
Fixes #2494

Co-authored-by: nyaoouo <64143453+nyaoouo@users.noreply.github.com>
Reviewed-on: OpenWF/SpaceNinjaServer#2500
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: nyaoouo <nyaoouo@noreply.localhost>
Co-committed-by: nyaoouo <nyaoouo@noreply.localhost>
2025-07-16 08:29:41 -07:00
fad1ee9314 chore(webui): update Chinese translation (#2501)
Reviewed-on: OpenWF/SpaceNinjaServer#2501
Co-authored-by: Corvus <corvus@noreply.localhost>
Co-committed-by: Corvus <corvus@noreply.localhost>
2025-07-16 08:29:13 -07:00
64b43fcccf chore(webui): fixing a mess in the translations (#2498)
Re #2494 (only fixes the strings, **NOT** the weapon issue)

- The invigoration stuff now mentions the numbers, percentages of buffs
- Improved some misleading strings (e.g. "Movement Speed", when it was in fact just "Sprint Speed" instead)
- Improved some inconsistencies in some key names (some weren't like other, similar existing ones)
- Got rid of duplicate "None" string & re-used it properly + re-used existing strings to newly added buttons, instead of using unnecessary extra added strings (more consistent to use existing strings, aside that they are shorter, less lines and less work overall for everyone involved)

If I should change anything, lemme know.

Reviewed-on: OpenWF/SpaceNinjaServer#2498
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-07-15 20:59:59 -07:00
e407262cf8 fix: don't send baro message ahead of his visit (#2497)
Closes #2495

Reviewed-on: OpenWF/SpaceNinjaServer#2497
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-15 20:59:49 -07:00
00e57c43df fix: charge correct amount of void traces for upgrading to radiant (#2492)
Closes #2490

Reviewed-on: OpenWF/SpaceNinjaServer#2492
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-15 20:59:39 -07:00
2ab9f39507 chore(webui): update Chinese translation (#2493)
Reviewed-on: OpenWF/SpaceNinjaServer#2493
Co-authored-by: Corvus <corvus@noreply.localhost>
Co-committed-by: Corvus <corvus@noreply.localhost>
2025-07-15 02:37:43 -07:00
b60723ef54 feat(webui): edit suit invigorations (#2478)
Co-authored-by: nyaoouo <64143453+nyaoouo@users.noreply.github.com>
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Reviewed-on: OpenWF/SpaceNinjaServer#2478
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: nyaoouo <nyaoouo@noreply.localhost>
Co-committed-by: nyaoouo <nyaoouo@noreply.localhost>
2025-07-14 20:33:37 -07:00
b3bf291d10 chore: send event messages for boosters (#2487)
Closes #2464

Reviewed-on: OpenWF/SpaceNinjaServer#2487
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-14 20:24:17 -07:00
db86e2d265 chore(webui): update translations (#2489)
- Updated German translation with all new strings
- Added the rest of the missing Chinese translation contributors (only the ones who did actually translate, that is)

Reviewed-on: OpenWF/SpaceNinjaServer#2489
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-07-14 20:23:56 -07:00
f6cb8414c1 chore(webui): refresh inventory when crafting/buying/gilding kitguns (#2486)
Reviewed-on: OpenWF/SpaceNinjaServer#2486
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-07-13 21:08:34 -07:00
ba3df4bdbc fix: don't give mastery xp for SpecialItems except for venari (#2484)
Closes #2482

Reviewed-on: OpenWF/SpaceNinjaServer#2484
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-13 21:08:23 -07:00
8feb3a5b3c feat: give kaithe summon at riding level 9 (#2483)
Closes #2480

Reviewed-on: OpenWF/SpaceNinjaServer#2483
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-13 21:08:14 -07:00
66f3d65d77 fix(webui): none syndicate (#2477)
Closes #2475

Reviewed-on: OpenWF/SpaceNinjaServer#2477
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-12 21:20:08 -07:00
b18f06087b chore(webui): update to Spanish translation (#2481)
Reviewed-on: OpenWF/SpaceNinjaServer#2481
Co-authored-by: hxedcl <hxedcl@noreply.localhost>
Co-committed-by: hxedcl <hxedcl@noreply.localhost>
2025-07-12 21:19:50 -07:00
987b5b98ff chore(webui): update Chinese translation (#2476)
Reviewed-on: OpenWF/SpaceNinjaServer#2476
Co-authored-by: Corvus <corvus@noreply.localhost>
Co-committed-by: Corvus <corvus@noreply.localhost>
2025-07-12 00:00:23 -07:00
fbbd9076cf fix: delete galleon of ghouls inbox messages when disabled via webui (#2473)
Reviewed-on: OpenWF/SpaceNinjaServer#2473
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-11 21:16:07 -07:00
838818543c fix: omit plains of eidolon from non-grineer sorties (#2472)
Closes #2470

Reviewed-on: OpenWF/SpaceNinjaServer#2472
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-11 21:15:48 -07:00
a16e2716f1 feat(webui): Change weapon Modular Parts (#2471)
Reviewed-on: OpenWF/SpaceNinjaServer#2471
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-07-11 21:15:16 -07:00
f4c7ce582b chore(webui): handle malformed rivens so they can be deleted at least (#2469)
Closes #2468

Reviewed-on: OpenWF/SpaceNinjaServer#2469
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-11 21:15:04 -07:00
c0187f9446 chore(webui): refresh inventory when pet was consigned (#2467)
Closes #2463

Reviewed-on: OpenWF/SpaceNinjaServer#2467
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-11 08:55:04 -07:00
f796f9a851 feat: resetQuestProgress (#2461)
Just giving the client an 'ok' response. It seems that it does use updateQuest to manage the state itself mostly, just the server and webui are a bit confused about a quest with all stages completed still being active.
Re #1323

Reviewed-on: OpenWF/SpaceNinjaServer#2461
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-10 20:59:39 -07:00
e18b8e09ea fix: properly track xp for modular items (#2460)
Closes #2454

Reviewed-on: OpenWF/SpaceNinjaServer#2460
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-10 20:59:32 -07:00
0d8044b87c chore(webui): update to Spanish translation (#2466)
Reviewed-on: OpenWF/SpaceNinjaServer#2466
Co-authored-by: hxedcl <hxedcl@noreply.localhost>
Co-committed-by: hxedcl <hxedcl@noreply.localhost>
2025-07-10 20:59:22 -07:00
a109ea6c5d chore: update PE+ (#2459)
Closes #2455

Reviewed-on: OpenWF/SpaceNinjaServer#2459
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-09 21:22:23 -07:00
7eb95c995c feat: initial invasions (#2458)
A rough generation of 3 invasions that change at daily reset, so missing the planet-based invasion 'chains'.
Battle pay is fully working tho, just a few points of uncertainty there due to missing research and logs.
Death marks are also roughly working.
Re #1097

Reviewed-on: OpenWF/SpaceNinjaServer#2458
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-09 19:58:01 -07:00
dc8f32d4d8 chore(webui): update Chinese translation (#2453)
Reviewed-on: OpenWF/SpaceNinjaServer#2453
Co-authored-by: Corvus <corvus@noreply.localhost>
Co-committed-by: Corvus <corvus@noreply.localhost>
2025-07-08 22:12:26 -07:00
ba70ba88dd fix(webui): recreate missing datalist-QuestKeys entries after refreshing inventory (#2452)
Closes #2448

Reviewed-on: OpenWF/SpaceNinjaServer#2452
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-08 20:52:45 -07:00
08d4a03c50 fix: use the correct magic number for crew member seeds (#2451)
Closes #2444

Reviewed-on: OpenWF/SpaceNinjaServer#2451
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-08 20:52:24 -07:00
45feff682b feat: give on call crew gear item for command rank 9 (#2450)
Closes #2445

Reviewed-on: OpenWF/SpaceNinjaServer#2450
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-08 20:52:14 -07:00
65be1083ce feat(webui): mark inbox as read (#2449)
Closes #1117

Reviewed-on: OpenWF/SpaceNinjaServer#2449
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-08 20:52:01 -07:00
07e7c9e897 fix: add eudico's post-new war yapping to allDialogue (#2447)
Reviewed-on: OpenWF/SpaceNinjaServer#2447
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-08 20:51:46 -07:00
dcb26471c9 feat: handle all slot types in inventorySlots.php (#2443)
Reviewed-on: OpenWF/SpaceNinjaServer#2443
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-08 20:51:35 -07:00
5a75d88385 feat: give skiajati and umbra mods alongside umbra, with max rank and potatoes (#2442)
Not 100% sure if the response format is correct and if this is even the correct time/place to do it, but impossible to say without a log from live.

Closes #1054

Reviewed-on: OpenWF/SpaceNinjaServer#2442
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-08 20:51:18 -07:00
a35572e306 fix(webui): move "add maxed" after "add" button (#2441)
Apparently the onclick event is being fired even when pressing enter. I originally moved it to make alt+enter add it maxed, but now even just pressing enter adds it maxed which is not what I wanted. :|

Reviewed-on: OpenWF/SpaceNinjaServer#2441
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-08 20:50:51 -07:00
c46c43f143 chore(webui): add loading string to translation system (#2440)
Closes #2439

Reviewed-on: OpenWF/SpaceNinjaServer#2440
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-08 20:50:28 -07:00
98ed2b5ee4 chore: use ideal time when going backwards to satisfy constraints (#2438)
"Before next expected world state refresh" is now used as a bare minimum constraint. If it cannot be met, we align to the ideal second. Compromising when multiple constraints are in use to avoid having to go back like 7 years, as this would break navigation.

Closes #2434

Reviewed-on: OpenWF/SpaceNinjaServer#2438
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-08 20:50:10 -07:00
7aa1b12306 fix: show multiplied relic reward amount on eom screen (#2437)
Reviewed-on: OpenWF/SpaceNinjaServer#2437
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-08 20:49:46 -07:00
b410f6b554 chore(webui): indicate unsaved changes (#2436)
Reviewed-on: OpenWF/SpaceNinjaServer#2436
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-08 20:49:22 -07:00
1dffcf979f feat: send tennokai email after WitW quest completion (#2433)
Reviewed-on: OpenWF/SpaceNinjaServer#2433
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-08 00:29:16 -07:00
c86bba017b chore(webui): update Chinese translation (#2432)
Reviewed-on: OpenWF/SpaceNinjaServer#2432
Co-authored-by: Corvus <corvus@noreply.localhost>
Co-committed-by: Corvus <corvus@noreply.localhost>
2025-07-08 00:29:05 -07:00
2c499cec3d fix: set proper dominant traits for helminth charger (#2429)
Closes #2417

Reviewed-on: OpenWF/SpaceNinjaServer#2429
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-08 00:28:56 -07:00
d6145561fd chore: improve randomness of void storm missions (#2428)
Instead of alternating the mission pool every hour, we now use sequentiallyUniqueRandomElement which should ensure that we don't duplicate any of the last x missions (x = 3 for Lith & Axi and x = 1 Meso & Neo).

Reviewed-on: OpenWF/SpaceNinjaServer#2428
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-08 00:28:49 -07:00
1545cdb8ce fix(webui): handle config having no worldState entry at all (#2427)
Reviewed-on: OpenWF/SpaceNinjaServer#2427
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-06 20:14:05 -07:00
80b5e2df7f feat: random recessive traits for beasts (#2426)
Reviewed-on: OpenWF/SpaceNinjaServer#2426
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-06 20:13:59 -07:00
76e61129bf fix: skip birthdays of characters we can't talk to (#2425)
Closes #2424

Reviewed-on: OpenWF/SpaceNinjaServer#2425
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-06 20:13:50 -07:00
ea3e299861 fix: ensure nightwave weekly challenges are unique (#2423)
Re #2411

Reviewed-on: OpenWF/SpaceNinjaServer#2423
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-06 20:13:42 -07:00
3d8c1d036a fix: ensure nightwave daily challenges are unique (#2422)
When generating a daily challenge, we now use sequentiallyUniqueRandomElement with a lookbehind of 2 to ensure the 2 previous (and still active) daily challenges are not duplicated.

Re #2411

Reviewed-on: OpenWF/SpaceNinjaServer#2422
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-06 20:13:32 -07:00
773f96ebbc fix: set PrimeTokenAvailability to true (#2420)
Closes #2416

Reviewed-on: OpenWF/SpaceNinjaServer#2420
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-06 20:13:20 -07:00
2a80307c26 chore(webui): reuse "code_remove" for "general_removeButton" (#2421)
Closes #2418

Reviewed-on: OpenWF/SpaceNinjaServer#2421
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-05 20:18:18 -07:00
a40ff27fea fix: add InitialStartDate to PrimeVaultTrader (#2419)
Closes #2414

Reviewed-on: OpenWF/SpaceNinjaServer#2419
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-05 20:18:01 -07:00
c9a4359714 chore(webui): update Chinese translation (#2413)
Reviewed-on: OpenWF/SpaceNinjaServer#2413
Co-authored-by: Corvus <corvus@noreply.localhost>
Co-committed-by: Corvus <corvus@noreply.localhost>
2025-07-05 16:52:16 -07:00
280ed8bef1 chore(webui): improve string (#2412)
The `100% chance` part is unnecessary in practice and can be shortened. In-game it also never mentions the `100% chance` part either.

Reviewed-on: OpenWF/SpaceNinjaServer#2412
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-07-05 06:41:26 -07:00
9c89e907b1 chore(webui): update Chinese translation (#2410)
Reviewed-on: OpenWF/SpaceNinjaServer#2410
Co-authored-by: Corvus <corvus@noreply.localhost>
Co-committed-by: Corvus <corvus@noreply.localhost>
2025-07-05 06:23:17 -07:00
b54fd96098 chore: fix cyclic includes due to saveConfig used in controllers (#2409)
Reviewed-on: OpenWF/SpaceNinjaServer#2409
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-05 06:23:10 -07:00
c7c7fd4ea0 chore: enforce consistent imports (#2408)
Reviewed-on: OpenWF/SpaceNinjaServer#2408
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-04 17:40:06 -07:00
a75e6d6b95 chore: add warning coverage for cyclic includes (#2407)
and some initial refactoring to avoid it where possible

Reviewed-on: OpenWF/SpaceNinjaServer#2407
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-04 16:49:25 -07:00
5089f67146 chore: improve ship customization stuff (#2402)
The only functionally relevant change is that orbiter scenes are now saved via SkinFlavourItem (as of U39?).
The rest is cleanup of the types because the ship customization stuff was duplicated all over the place.

Reviewed-on: OpenWF/SpaceNinjaServer#2402
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-04 15:19:15 -07:00
0416221d15 feat: reset custom obstable course leaderboard (#2401)
Reviewed-on: OpenWF/SpaceNinjaServer#2401
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-04 15:19:03 -07:00
26729ce21a feat: railjack skins (#2400)
Technically worked before but some weird behaviour. Also updating saveLoadout again. I think warn is a more appropriate severity. It's certainly not a progession stopper if some category is unimplemented.

Closes #2397

Reviewed-on: OpenWF/SpaceNinjaServer#2400
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-04 15:18:53 -07:00
ee4adc7d55 feat: Varzia (Prime Resurgence) rotation (#2390)
Also closes #1059

Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Reviewed-on: OpenWF/SpaceNinjaServer#2390
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-07-04 15:18:41 -07:00
29aadf4e78 chore(webui): improve string (#2406)
other `after Hacking` or `while Hacking` strings are in uppercase, but this particular one wasn't

Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Reviewed-on: OpenWF/SpaceNinjaServer#2406
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-07-04 15:02:30 -07:00
0b32bc21be chore(webui): improve string (#2405)
was missing a plus and this shorter version may fit better

Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Reviewed-on: OpenWF/SpaceNinjaServer#2405
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-07-04 04:55:36 -07:00
e09e5ebec2 fix: infinite loop when attempting to generate loid commisions (#2399)
Reviewed-on: OpenWF/SpaceNinjaServer#2399
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-04 03:26:41 -07:00
b2de8608c6 chore: fix duplicate import 2025-07-04 11:46:49 +02:00
2b23db1433 chore: remove usage of markModified (#2403)
Only this one remained, but not needed because it's schema'd.

Reviewed-on: OpenWF/SpaceNinjaServer#2403
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-03 22:45:07 -07:00
a45bacc388 chore: update PE+ (#2398)
Closes #2392

Reviewed-on: OpenWF/SpaceNinjaServer#2398
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-03 22:44:26 -07:00
46d37d3688 chore(webui): update Chinese translation (#2396)
Reviewed-on: OpenWF/SpaceNinjaServer#2396
Co-authored-by: Corvus <corvus@noreply.localhost>
Co-committed-by: Corvus <corvus@noreply.localhost>
2025-07-03 11:09:07 -07:00
41686aea88 fix: properly cap negative syndicate standing (#2393)
Closes #2388

Reviewed-on: OpenWF/SpaceNinjaServer#2393
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-03 11:08:57 -07:00
61ac2f8b72 chore(webui): update Chinese translation (#2394)
Reviewed-on: OpenWF/SpaceNinjaServer#2394
Co-authored-by: Corvus <corvus@noreply.localhost>
Co-committed-by: Corvus <corvus@noreply.localhost>
2025-07-02 15:04:42 -07:00
d2ab894c01 fix(webui): assign labels for appropriate inputs (#2391)
Reviewed-on: OpenWF/SpaceNinjaServer#2391
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-07-02 14:29:41 -07:00
8c85cdcd1d feat(webui): "add maxed" for mods (#2387)
Closes #2382

Reviewed-on: OpenWF/SpaceNinjaServer#2387
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-02 14:18:24 -07:00
aa6191f033 feat: relic rng cheats (#2386)
Closes #2370

Reviewed-on: OpenWF/SpaceNinjaServer#2386
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-02 14:17:14 -07:00
e26d2635fb chore(webui): fix inconsistent % chance strings (#2385)
Closes #2381

Reviewed-on: OpenWF/SpaceNinjaServer#2385
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-02 14:15:50 -07:00
dd6ae8898f chore(webui): move max rank button before detailed view link (#2384)
Reviewed-on: OpenWF/SpaceNinjaServer#2384
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-02 14:14:19 -07:00
499ca23ffb chore(webui): update inventory when equipment was forma'd (#2383)
Reviewed-on: OpenWF/SpaceNinjaServer#2383
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-02 14:14:13 -07:00
d3102acb7c chore(webui): update Chinese translation (#2380)
Reviewed-on: OpenWF/SpaceNinjaServer#2380
Co-authored-by: Corvus <corvus@noreply.localhost>
Co-committed-by: Corvus <corvus@noreply.localhost>
2025-07-01 10:58:47 -07:00
363028c9ce chore: clarify log output related to saveLoadout (#2379)
Reviewed-on: OpenWF/SpaceNinjaServer#2379
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-01 07:48:03 -07:00
1d60745f18 feat: year rollover kiss emails (#2376)
Closes #2375

Reviewed-on: OpenWF/SpaceNinjaServer#2376
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-01 07:45:41 -07:00
a9b3b16d31 feat: dojo visitors (#2374)
Closes #2373

Reviewed-on: OpenWF/SpaceNinjaServer#2374
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-01 07:45:12 -07:00
fd1d72a1cf chore(webui): add inventory update note to quests tab (#2372)
Reviewed-on: OpenWF/SpaceNinjaServer#2372
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-01 07:44:30 -07:00
75832afdbe feat: relicRewardItemCountMultiplier cheat (#2369)
Reviewed-on: OpenWF/SpaceNinjaServer#2369
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-01 07:44:13 -07:00
aa916d2820 feat: sell genetic imprints (#2368)
Reviewed-on: OpenWF/SpaceNinjaServer#2368
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-01 07:44:05 -07:00
5a5f6106a3 chore: save inventory and account in parallel when claiming login reward (#2371)
Reviewed-on: OpenWF/SpaceNinjaServer#2371
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-30 17:55:06 -07:00
24d9dc27e2 chore(webui): update inventory when login reward was claimed (#2367)
Closes #2360

Reviewed-on: OpenWF/SpaceNinjaServer#2367
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-30 13:30:09 -07:00
5e05a15743 fix: update calendar progress at daily reset (#2365)
Closes #2364

Reviewed-on: OpenWF/SpaceNinjaServer#2365
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-30 13:28:27 -07:00
545b949202 feat: sell crew members (#2366)
Closes #2363

Reviewed-on: OpenWF/SpaceNinjaServer#2366
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-30 12:58:24 -07:00
0c9b27a29b chore: optimise creditsController (#2359)
Doing both lookups in parallel saves around 1 ms in the happy case (20% of baseline time), and in case nonce does not match, the error is simply raised as per usual with the inventory request being lightweight enough to be negligible.

Noteworthy that this reasoning doesn't really work for other controllers because in the error case, the inventory request would still be quite significant, even if the HTTP request itself would still finish quickly.

Reviewed-on: OpenWF/SpaceNinjaServer#2359
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-30 11:05:49 -07:00
cfa750b6f7 fix: handle crafting of archwing summon for versions prior to U39 (#2358)
Closes #2356

Reviewed-on: OpenWF/SpaceNinjaServer#2358
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-30 11:05:16 -07:00
049baa4313 fix(webui): make sidebar sticky as intended (#2354)
also a bit of language-specific width adjustment

Reviewed-on: OpenWF/SpaceNinjaServer#2354
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-30 11:04:58 -07:00
e267ca8f55 chore(webui): update to Spanish translation (#2355)
Reviewed-on: OpenWF/SpaceNinjaServer#2355
Co-authored-by: hxedcl <hxedcl@noreply.localhost>
Co-committed-by: hxedcl <hxedcl@noreply.localhost>
2025-06-29 19:42:24 -07:00
1a2d8ab19a chore: continue execution if subsequent JSON.parse on config failed (#2353)
By calling JSON.parse before setting everything to undefined, we don't lose the old config in case of an error.

Reviewed-on: OpenWF/SpaceNinjaServer#2353
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-29 11:54:20 -07:00
8c19aec340 fix(webui): "all focus schools maxed out" doesn't have squad regen maxed 2025-06-29 19:32:49 +02:00
d1c860c693 feat(webui): Valence Bonus (#2348)
Closes #1181

Reviewed-on: OpenWF/SpaceNinjaServer#2348
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-06-29 09:28:28 -07:00
69f9d5ebc5 fix: handle setPlacedDecoInfo for non-ship boot locations (#2349)
Closes #2347

Reviewed-on: OpenWF/SpaceNinjaServer#2349
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-29 09:08:44 -07:00
d66f1c58d8 feat(webui): light/dark theme selection (#2343)
Closes #2093

Reviewed-on: OpenWF/SpaceNinjaServer#2343
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-29 09:07:53 -07:00
5234cf213e chore(webui): explain star chart refreshing for unlock all missions (#2342)
This is relevant for U39 at least

Reviewed-on: OpenWF/SpaceNinjaServer#2342
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-29 09:07:33 -07:00
7c7d2b9061 chore(webui): update to Spanish translation (#2351)
Reviewed-on: OpenWF/SpaceNinjaServer#2351
Co-authored-by: hxedcl <hxedcl@noreply.localhost>
Co-committed-by: hxedcl <hxedcl@noreply.localhost>
2025-06-29 09:07:12 -07:00
3d46d05a6c chore(webui): update Chinese translation (#2350)
Reviewed-on: OpenWF/SpaceNinjaServer#2350
Co-authored-by: Corvus <corvus@noreply.localhost>
Co-committed-by: Corvus <corvus@noreply.localhost>
2025-06-29 09:07:05 -07:00
00cea6788e fix: sync config with db only after connection is established (#2346)
The error message had regressed in the case where connection could not be established.

Reviewed-on: OpenWF/SpaceNinjaServer#2346
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-28 15:08:36 -07:00
58bdb2d2ec chore(webui): improving inconsistent, long string (#2344)
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Reviewed-on: OpenWF/SpaceNinjaServer#2344
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-06-28 10:28:09 -07:00
c4c622d82b feat: baro's void surplus (#2334)
Closes #2284

Reviewed-on: OpenWF/SpaceNinjaServer#2334
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-28 09:50:40 -07:00
44a129ab0b feat: create genetic imprint (#2337)
Re #2212

Reviewed-on: OpenWF/SpaceNinjaServer#2337
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-28 09:48:12 -07:00
5a7caa5ba9 feat: disableDailyTribute config (#2338)
Reviewed-on: OpenWF/SpaceNinjaServer#2338
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-28 09:47:39 -07:00
a9c5e30994 chore: deal with visiting navigation not resyncing inventory (#2340)
Closes #2339

Reviewed-on: OpenWF/SpaceNinjaServer#2340
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-28 09:46:45 -07:00
f0547cb9e6 chore(webui): update Chinese translation (#2341)
Reviewed-on: OpenWF/SpaceNinjaServer#2341
Co-authored-by: Corvus <corvus@noreply.localhost>
Co-committed-by: Corvus <corvus@noreply.localhost>
2025-06-28 09:45:45 -07:00
ef3d3b92c7 feat: darvo deal (#2261)
Closes #2260

Reviewed-on: OpenWF/SpaceNinjaServer#2261
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-28 09:39:41 -07:00
a9359bd989 feat(webui): the circuit override (#2335)
Re #2312

Reviewed-on: OpenWF/SpaceNinjaServer#2335
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-27 20:28:44 -07:00
3d21813a79 fix(vscode): update launch.json 2025-06-28 01:28:45 +02:00
7cad831702 chore: update PE+ (#2336)
Closes #2332

Reviewed-on: OpenWF/SpaceNinjaServer#2336
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-27 15:16:22 -07:00
0f2b6c68cd chore: use some instead of find 2025-06-27 19:29:55 +02:00
4fcac6dc37 feat: duviri murmur reward tiers (#2331)
Reviewed-on: OpenWF/SpaceNinjaServer#2331
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-27 08:22:00 -07:00
d2cae012a7 chore: add operation eight claw trophies to allDecoRecipes (#2330)
Closes #2295

Reviewed-on: OpenWF/SpaceNinjaServer#2330
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-27 08:21:48 -07:00
b36d524953 feat: personal deco capacity costs (#2329)
Closes #2278

Reviewed-on: OpenWF/SpaceNinjaServer#2329
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-27 08:21:27 -07:00
abb5b8880f chore(webui): keep config in sync with multiple tabs (#2325)
Adding "wsid" to uniquely identify a given tab (by the websocket connection) so we can avoid needless refreshing on the same tab.

Closes #2316

Reviewed-on: OpenWF/SpaceNinjaServer#2325
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-27 08:21:05 -07:00
4895b4630b feat: credit boosters (+ daily first win) (#2324)
Daily first win is kinda weird because the client doesn't even seem to acknowledge it.

Also fixed missionCompletionCredits being added to inventory inconsistently (sometimes once, sometimes twice).

Closes #1086
Closes #2322

Reviewed-on: OpenWF/SpaceNinjaServer#2324
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-27 08:20:37 -07:00
690b872b5e chore(webui): update Chinese translation (#2328)
Reviewed-on: OpenWF/SpaceNinjaServer#2328
Co-authored-by: Corvus <corvus@noreply.localhost>
Co-committed-by: Corvus <corvus@noreply.localhost>
2025-06-26 22:26:35 -07:00
d77fe60cd8 fix(webui): apply consistent margins (#2327)
Reviewed-on: OpenWF/SpaceNinjaServer#2327
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-26 22:26:22 -07:00
3cae42c7d6 fix: acrithis vendor freezing with fullyStockedVendors (#2326)
Permanent offers were not satisfying bin constraints

Reviewed-on: OpenWF/SpaceNinjaServer#2326
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-26 22:25:31 -07:00
bbccee0637 fix: ignore purchaseQuantity for login reward items (#2321)
cryotic amount should not be multiplied by 3000...

Reviewed-on: OpenWF/SpaceNinjaServer#2321
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-26 19:40:06 -07:00
31e24c27ad chore: ignore invalid item ids in saveLoadout (#2320)
With the 'IsNew' flag + webui delete item, this is quite easy to trigger and shouldn't prevent the other changes from going through.

Reviewed-on: OpenWF/SpaceNinjaServer#2320
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-26 19:39:46 -07:00
4acd87aae6 chore: handle CalendarProgress in updateChallengeProgress 2025-06-26 19:35:03 -07:00
d8ff601be7 fix: array out of bounds when processing CalendarProgress 2025-06-26 19:35:03 -07:00
d79e7c0274 feat(webui): world state config (#2318)
Re #2312. Will need some follow-up considerations for circuit game modes.

Reviewed-on: OpenWF/SpaceNinjaServer#2318
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-26 19:32:53 -07:00
4f1f9592b0 chore: use chokidar for configWatcherService (#2315)
It's just a lot snappier + works flawlessly under Bun.

Reviewed-on: OpenWF/SpaceNinjaServer#2315
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-26 15:30:01 -07:00
764cdd1ab8 feat: worldState.allTheFissures (#2313)
Closes #2294

Reviewed-on: OpenWF/SpaceNinjaServer#2313
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-26 14:32:26 -07:00
0ba641a2ac chore: update PE+ (#2311)
Closes #2309

Reviewed-on: OpenWF/SpaceNinjaServer#2311
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-26 14:31:52 -07:00
eb7b51852b fix: use exact quantity when adding gear items by StoreItem (#2310)
Closes #2304

Reviewed-on: OpenWF/SpaceNinjaServer#2310
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-26 14:31:40 -07:00
a3be376489 chore(webui): add Thalys to Incarnon List (#2299)
Closes #2298

Reviewed-on: OpenWF/SpaceNinjaServer#2299
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-06-26 11:02:40 -07:00
d94cd38120 chore(webui): update Chinese translation (#2291)
Reviewed-on: OpenWF/SpaceNinjaServer#2291
Co-authored-by: Corvus <corvus@noreply.localhost>
Co-committed-by: Corvus <corvus@noreply.localhost>
2025-06-26 06:44:03 -07:00
8c22555904 feat(webui): add missing subsumed abilities (#2287)
Closes #1984

Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Reviewed-on: OpenWF/SpaceNinjaServer#2287
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-06-25 20:24:29 -07:00
c9edef39f8 feat: claimJunctionChallengeReward (#2289)
Closes #2285

Reviewed-on: OpenWF/SpaceNinjaServer#2289
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-25 20:24:16 -07:00
b42182c85f fix(webui): handle existing entries for unlock all missions (#2290)
Closes #2283

Reviewed-on: OpenWF/SpaceNinjaServer#2290
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-25 20:22:24 -07:00
86f86d0476 chore: fully adopt tsgo
It's finally able to emit this project without issues. It doesn't yet support incremental builds, but a full build with it is faster than an incremental build with the old tsc, so we're not losing anything.
2025-06-26 05:20:29 +02:00
0fdf8b2c75 chore(webui): update fr
Co-authored-by: Vitruvio <vitruvio@noreply.localhost>
2025-06-25 19:33:21 +02:00
285b1bbf60 chore(webui): update Chinese translation (#2281)
Reviewed-on: OpenWF/SpaceNinjaServer#2281
Co-authored-by: Corvus <corvus@noreply.localhost>
Co-committed-by: Corvus <corvus@noreply.localhost>
2025-06-25 08:04:46 -07:00
731ce6c215 feat: galleon of ghouls (#2280)
Re #1103

Reviewed-on: OpenWF/SpaceNinjaServer#2280
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-25 08:04:03 -07:00
39630c5af7 fix: properly convert personal room decos to and from inventory types (#2279)
Closes #2277

Reviewed-on: OpenWF/SpaceNinjaServer#2279
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-25 08:03:56 -07:00
d5be202835 fix: ensure every bounty tier has a unique job type (#2273)
I saw "trash their traps" show up twice on Eudico in different tiers, I don't think that's correct.

Reviewed-on: OpenWF/SpaceNinjaServer#2273
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-25 08:03:49 -07:00
3a6e4ac2e1 feat: Arcana Isolation Vault rewards (#2276)
Closes #388

Reviewed-on: OpenWF/SpaceNinjaServer#2276
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-06-24 19:05:19 -07:00
e234af098d fix(webui): incorrect value of upgrade_AvatarTimeLimitIncrease string (#2274)
Reviewed-on: OpenWF/SpaceNinjaServer#2274
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-24 17:12:39 -07:00
4a434cea2b chore(webui): update to Spanish translation (#2275)
Reviewed-on: OpenWF/SpaceNinjaServer#2275
Co-authored-by: hxedcl <hxedcl@noreply.localhost>
Co-committed-by: hxedcl <hxedcl@noreply.localhost>
2025-06-24 12:02:21 -07:00
36f2828d37 feat: void trader (#2269)
Closes #2245

Reviewed-on: OpenWF/SpaceNinjaServer#2269
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-24 11:19:32 -07:00
ca3cfb5299 feat(webui): max focus schools (#2270)
Closes #1433

Reviewed-on: OpenWF/SpaceNinjaServer#2270
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-24 10:03:18 -07:00
f242d9f873 chore: make ws self test work under bun (#2268)
Reviewed-on: OpenWF/SpaceNinjaServer#2268
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-23 21:52:02 -07:00
9a034b1c8a feat: unfaithful bug fixes (#2267)
Closes #2257

Reviewed-on: OpenWF/SpaceNinjaServer#2267
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-23 21:51:48 -07:00
122950034e chore: cleanup purchase stuff (#2266)
Reviewed-on: OpenWF/SpaceNinjaServer#2266
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-23 21:51:09 -07:00
636d3100f3 fixup for 444c92f0c60d7bafc320b487105d610f0e1ff6af
I forgot to save this file
2025-06-24 01:34:47 +02:00
444c92f0c6 fix: use shared count for calendar day indecies (#2265)
I'm not sure if this was always this way and I was just really confused when I initially implemented this, or if this was changed in a later version, but at least now it seems to be tracking everything correctly for 38.6.0.

Closes #2264

Reviewed-on: OpenWF/SpaceNinjaServer#2265
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-23 15:02:30 -07:00
653798b987 fix: use correct dropTable for bounty stage reward (#2263)
Re #388

Reviewed-on: OpenWF/SpaceNinjaServer#2263
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-06-23 14:56:18 -07:00
7a88f6f486 chore: create AGENTS.md (#2262)
Reviewed-on: OpenWF/SpaceNinjaServer#2262
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-23 14:56:08 -07:00
82b203e00b fix(nemesis): subtract charge from installed mods instead of ideal mods (#2259)
Because oull might substitute one of them.

Closes #2258

Reviewed-on: OpenWF/SpaceNinjaServer#2259
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-23 14:55:48 -07:00
271f5bd47a fix: also increment LastCompletedDayIdx when completing a 1999 challenge (#2256)
Fixes #2255

Reviewed-on: OpenWF/SpaceNinjaServer#2256
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-23 04:58:21 -07:00
f61d15b496 chore: replace 'websocket' with 'undici' (#2253)
This is a lot more lightweight

Reviewed-on: OpenWF/SpaceNinjaServer#2253
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-23 04:55:19 -07:00
cfd50e7402 feat: unlockAllSimarisResearchEntries cheat (#2252)
Closes #1869

Reviewed-on: OpenWF/SpaceNinjaServer#2252
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-23 04:54:54 -07:00
2421a16b2c fix: cap spy rotations at C (#2251)
for Jade Shadows' spy mission with 4 vaults. will simply do ABCC in this case.

Closes #2250

Reviewed-on: OpenWF/SpaceNinjaServer#2251
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-23 04:54:36 -07:00
cee622d5e9 chore: add bun support (#2254)
It definitely has some benefits:
- It starts up insanely quickly compared to Node.
- It can run typescript directly, allow the build step to be reduced to verify/noEmit.

It does not implement NodeJS APIs perfectly, so I've had to add some special handling for Bun, but I think that's okay.

Reviewed-on: OpenWF/SpaceNinjaServer#2254
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-22 19:58:48 -07:00
84f081312b feat: fullyStockedVendors cheat (#2246)
Reviewed-on: OpenWF/SpaceNinjaServer#2246
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-22 06:55:44 -07:00
7ca7147b78 fix(docker): install node-gyp deps to fix arm64 build 2025-06-22 15:52:37 +02:00
558af66965 chore: verify that httpsPort has actually been bound by us (#2243)
I'm not the biggest fan of this, but it's a semi-regular problem and this should help affected users quickly discover it.

Reviewed-on: OpenWF/SpaceNinjaServer#2243
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-22 06:41:43 -07:00
d7b9fb1ab5 fix(webui): once awake stage 2 not consistently showing up in-game (#2244)
Fixes #2040

Reviewed-on: OpenWF/SpaceNinjaServer#2244
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-22 06:41:06 -07:00
bf12f90c88 chore: replace unlockAllMissions config with an account cheats button (#2241)
This way, mission completion rewards are given. This is especially import for junction rewards like quest keys (Closes #2229).

Reviewed-on: OpenWF/SpaceNinjaServer#2241
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-22 06:37:17 -07:00
6dd9b42f40 feat(webui): update inventory when in-game changes are made (#2239)
A bit of a rough initial implementation, but already works pretty well.

Closes #2224

Reviewed-on: OpenWF/SpaceNinjaServer#2239
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-22 06:36:47 -07:00
3bcd5827f9 chore(webui): unset nonce when logging out (#2242)
Reviewed-on: OpenWF/SpaceNinjaServer#2242
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-21 14:38:48 -07:00
d16d763977 chore: handle logout POST request for older versions (#2240)
Reviewed-on: OpenWF/SpaceNinjaServer#2240
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-21 14:38:24 -07:00
ff8ec8dbed chore(webui): update bootstrap, add sourcemaps for it (#2238)
Reviewed-on: OpenWF/SpaceNinjaServer#2238
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-21 14:38:04 -07:00
6cdd103c3d chore(dev): improve bulk change handling (#2234)
Fixed abandoned build processes sometimes still triggering a start (causing double-starts) made it more robust in regards to webui changes being intermixed: making the fetch a fire-and-forget to avoid errors, and waiting for the websocket connection to be re-established to avoid the browser attempting to reload when the server may not be up for a few seconds.

Reviewed-on: OpenWF/SpaceNinjaServer#2234
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-21 11:33:59 -07:00
b6f79c1e5c fix: save steel path mission completion (#2233)
Fixes #2228

Reviewed-on: OpenWF/SpaceNinjaServer#2233
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-21 11:25:42 -07:00
2bb3e2afdd chore(webui): update to Spanish translation (#2236)
Reviewed-on: OpenWF/SpaceNinjaServer#2236
Co-authored-by: hxedcl <hxedcl@noreply.localhost>
Co-committed-by: hxedcl <hxedcl@noreply.localhost>
2025-06-21 10:51:57 -07:00
6a60537cd0 chore: remove unused string
Fixup for 2fa6dcc7edb34c9382c31739d8b61a84803d69c2
2025-06-21 17:46:38 +02:00
2fa6dcc7ed feat(webui): handle auth via websocket (#2226)
Now when logging in and out of the game, the webui is notified so it can refresh the nonce, removing the need for constant login requests to revalidate it.

Closes #2223

Reviewed-on: OpenWF/SpaceNinjaServer#2226
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-21 07:26:43 -07:00
93ef9a5348 feat: autogenerate all vendors (#2225)
I went through a few of the still-hardcoded vendors and they seemed to "just work" with autogeneration so I think it's time to say: Closes #1225 and disable the `noVendorPurchaseLimits` cheat by default.

Reviewed-on: OpenWF/SpaceNinjaServer#2225
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-21 07:26:11 -07:00
5d5d0ee560 chore(webui): update Chinese translation (#2227)
Reviewed-on: OpenWF/SpaceNinjaServer#2227
Co-authored-by: CrazyZhang <crazyzhang@noreply.localhost>
Co-committed-by: CrazyZhang <crazyzhang@noreply.localhost>
2025-06-21 07:25:48 -07:00
f84cc54c97 chore: use build & start process for development as well (#2222)
Reviewed-on: OpenWF/SpaceNinjaServer#2222
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-20 18:30:52 -07:00
4cb0f8b167 feat(webui): initial websocket integration to be more responsive (#2221)
For now just handles changes to config.json but in the future might keep the inventory tabs up-to-date with in-game actions.

Reviewed-on: OpenWF/SpaceNinjaServer#2221
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-20 14:00:55 -07:00
eadc9c4ecb feat(webui): max rank plexus (#2219)
Closes #1740

Reviewed-on: OpenWF/SpaceNinjaServer#2219
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-20 14:00:39 -07:00
f41377bb81 chore: npm audit fix (#2220)
Reviewed-on: OpenWF/SpaceNinjaServer#2220
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-20 09:40:53 -07:00
95136e6059 feat: dynamic void fissure missions (#2214)
Closes #1512

Reviewed-on: OpenWF/SpaceNinjaServer#2214
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-20 04:47:45 -07:00
3c64f17e34 feat: missionsCanGiveAllRelics cheat (#2217)
Closes #1060

Reviewed-on: OpenWF/SpaceNinjaServer#2217
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-20 04:42:11 -07:00
3619bdfdb5 feat: autogenerate mask vendor (#2216)
Reviewed-on: OpenWF/SpaceNinjaServer#2216
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-20 04:41:47 -07:00
97064826b2 feat: unlockAllProfitTakerStages cheat (#2215)
Closes #2081

Reviewed-on: OpenWF/SpaceNinjaServer#2215
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-20 04:41:13 -07:00
c6c7a2966b fix: deimos vault bounty detection (#2207)
Related to #388
this should fix incorect rewards for deimos filed bounties

Reviewed-on: OpenWF/SpaceNinjaServer#2207
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-06-20 04:40:27 -07:00
ce46fa14ac chore: ignore crossPlaySetting in updateChallengeProgress 2025-06-20 05:04:24 +02:00
3186ffe164 feat: autogenerate temple & archimedean vendors (#2208)
So the kuva offer is refreshed every week.

Reviewed-on: OpenWF/SpaceNinjaServer#2208
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-19 19:39:50 -07:00
e686a2d028 chore: report unknown fields in updateChallengeProgress payload (#2210)
Reviewed-on: OpenWF/SpaceNinjaServer#2210
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-19 16:18:49 -07:00
88d4ba6340 feat: dontSubtractVendor{Credit,Platinum,Item,Standing}Cost cheats (#2209)
Closes #1586

Reviewed-on: OpenWF/SpaceNinjaServer#2209
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-19 16:18:35 -07:00
05382beaaf feat: worldState.duviriOverride config (#2206)
Closes #2205

Reviewed-on: OpenWF/SpaceNinjaServer#2206
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-19 16:18:17 -07:00
e136c0494e chore(webui): update to Spanish translation (#2213)
Reviewed-on: OpenWF/SpaceNinjaServer#2213
Co-authored-by: hxedcl <hxedcl@noreply.localhost>
Co-committed-by: hxedcl <hxedcl@noreply.localhost>
2025-06-19 13:38:00 -07:00
ad7b5fc052 fix(webui): incorrect description of topaz shields for blast (#2211)
Reviewed-on: OpenWF/SpaceNinjaServer#2211
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-19 11:15:27 -07:00
61a8d01f64 ci: split docker amd64 & arm64 builds 2025-06-19 14:00:16 +02:00
d78ca91d6c fix(webui): properly handle renaming of pets (#2204)
Reviewed-on: OpenWF/SpaceNinjaServer#2204
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-19 04:23:33 -07:00
4ca4990f89 chore(docker): use file-based config & precompile code in image (#2202)
Reviewed-on: OpenWF/SpaceNinjaServer#2202
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-19 04:23:10 -07:00
bf40155dd4 chore: no-op nemesis mode=w (#2196)
Reviewed-on: OpenWF/SpaceNinjaServer#2196
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-19 04:22:54 -07:00
2e9d3c33b6 chore(webui): update to Spanish translation (#2203)
Reviewed-on: OpenWF/SpaceNinjaServer#2203
Co-authored-by: hxedcl <hxedcl@noreply.localhost>
Co-committed-by: hxedcl <hxedcl@noreply.localhost>
2025-06-18 11:48:22 -07:00
7c8e8fe049 ci: only run on pushes to main
Non-main branches are gonna have to open a PR anyway, so we don't need to run it twice.
2025-06-18 20:42:27 +02:00
0c4065619d chore: support config path being specified via command line argument (#2201)
Reviewed-on: OpenWF/SpaceNinjaServer#2201
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-18 11:34:12 -07:00
dabca46e88 feat(webui): automatically commit toggle changes (#2198)
Closes #2197

Reviewed-on: OpenWF/SpaceNinjaServer#2198
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-18 11:05:07 -07:00
7819d87bbe chore: update nodejs version in Dockerfile (#2199)
Reviewed-on: OpenWF/SpaceNinjaServer#2199
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-18 10:23:39 -07:00
b8b8b6a6c6 feat: railjack valence fusion (#2194)
Closes #1678

Reviewed-on: OpenWF/SpaceNinjaServer#2194
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-18 05:52:16 -07:00
9af0e06b70 feat: add worldState.circuitGameModes config option (#2192)
Closes #749

Reviewed-on: OpenWF/SpaceNinjaServer#2192
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-18 05:51:56 -07:00
f8d0c9e0cb chore: use ChallengesFixVersion that client provides (#2190)
It seems that now we're on version 7, so let's just not hard-code it.

Reviewed-on: OpenWF/SpaceNinjaServer#2190
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-18 05:51:20 -07:00
16e80acb53 chore: add return type to createMessage (#2188)
Reviewed-on: OpenWF/SpaceNinjaServer#2188
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-18 05:50:43 -07:00
6691d4e402 feat: autogenerate steel path honors vendor (#2187)
No more "preprocessing" needed now. Some good progress for #1225, I'd say.

Reviewed-on: OpenWF/SpaceNinjaServer#2187
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-18 05:49:58 -07:00
3dcd2663d3 fix: weaken classic lich when getting all 3 mods correct (#2186)
Fixes #2185

Reviewed-on: OpenWF/SpaceNinjaServer#2186
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-18 05:39:10 -07:00
ffeffe2796 chore(webui): update es (#2195)
Reviewed-on: OpenWF/SpaceNinjaServer#2195
Co-authored-by: hxedcl <hxedcl@noreply.localhost>
Co-committed-by: hxedcl <hxedcl@noreply.localhost>
2025-06-18 05:18:02 -07:00
a9f1368cb7 fix: add UpgradeType field to repaired railjack weapons (#2193)
Reviewed-on: OpenWF/SpaceNinjaServer#2193
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-17 20:58:10 -07:00
cccf6f04a5 feat: xtra cheese rotation (#2191)
This has a bug where the client shows a negative time for "Xtra cheese starts in ..." until it refreshes the world state. This is because we're only providing the new activation as soon as that time/date is reached. However, this is 100% faithful to live.

Reviewed-on: OpenWF/SpaceNinjaServer#2191
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-17 17:32:34 -07:00
1ead581780 chore: improve typing of IFocusXp (#2182)
Any given focus school can be undefined in this object due to importing.

Reviewed-on: OpenWF/SpaceNinjaServer#2182
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-17 05:02:58 -07:00
145d21e30e fix: weaken infested lich (#2181)
Closes #2180

Reviewed-on: OpenWF/SpaceNinjaServer#2181
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-17 05:02:34 -07:00
6c2055a246 feat: echoes of umbra (#2177)
Having this item in the inventory unlocks the helminth option which is helpfully called "remove cyst" to install and uninstall it on a frame.

Reviewed-on: OpenWF/SpaceNinjaServer#2177
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-17 05:02:11 -07:00
01e490768c fix: ensure helminth shard operations don't produce a null shard (#2176)
Reviewed-on: OpenWF/SpaceNinjaServer#2176
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-17 05:01:55 -07:00
2e8fe799d7 feat: setSuitInfection (#2174)
Closes #2172

Reviewed-on: OpenWF/SpaceNinjaServer#2174
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-17 05:01:21 -07:00
53976378bb chore(webui): update Chinese translation (#2184)
Reviewed-on: OpenWF/SpaceNinjaServer#2184
Co-authored-by: qianlishun <qianlishun@noreply.localhost>
Co-committed-by: qianlishun <qianlishun@noreply.localhost>
2025-06-17 03:28:49 -07:00
4e832d3b2c chore(webui): add login/register error messages to translation system (#2179)
Closes #2178

Reviewed-on: OpenWF/SpaceNinjaServer#2179
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-16 15:05:26 -07:00
8c1147998d fix(webui): respond with 200 for successful shard operations (#2175)
Reviewed-on: OpenWF/SpaceNinjaServer#2175
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-16 14:55:56 -07:00
3e99e069be feat: void storm rotation (#2171)
Re #1512

Reviewed-on: OpenWF/SpaceNinjaServer#2171
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-16 14:55:35 -07:00
9731004de6 feat: autogenerate railjack crew member vendor (#2170)
Reviewed-on: OpenWF/SpaceNinjaServer#2170
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-16 14:51:47 -07:00
b98a88b700 chore: retroactively populate vendor to hide that it was just generated (#2168)
For example, debt-bonds at ticker always expire in at least 2 hours so visiting him, you'd never see an offer with an expiry less than that. The solution here is simply generating offers for the last few hours.

Reviewed-on: OpenWF/SpaceNinjaServer#2168
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-15 18:52:10 -07:00
6023f1c113 feat: autogenerate "today's special" at mining vendors (#2167)
Reviewed-on: OpenWF/SpaceNinjaServer#2167
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-15 18:51:42 -07:00
c6dd8bfb81 chore: improve error reporting when config.json exists with invalid json (#2166)
Reviewed-on: OpenWF/SpaceNinjaServer#2166
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-15 18:51:32 -07:00
3053112428 chore: auto-generate palladino's vendor manifest (#2160)
A bit ugly, but having the self test forces correctness.

Reviewed-on: OpenWF/SpaceNinjaServer#2160
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-15 05:53:10 -07:00
f448d03880 fix: 1999 bounty chemistry (#2164)
Closes #2162

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Reviewed-on: OpenWF/SpaceNinjaServer#2110
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-03 10:40:28 -07:00
d43e39d7b5 fix(webui): error when an unknown suit is max rank (#2109)
Reviewed-on: OpenWF/SpaceNinjaServer#2109
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-02 01:44:10 -07:00
b0499a62aa chore: use 'git checkout -f' instead of 'git reset --hard' to avoid loss 2025-06-01 12:16:16 +02:00
8f02bd1509 fix: avoid addition by undefined when adding skill points (#2108)
Reviewed-on: OpenWF/SpaceNinjaServer#2108
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-01 03:12:14 -07:00
32f4c5105a feat: add update and start script for linux (#2107)
Reviewed-on: OpenWF/SpaceNinjaServer#2107
Co-authored-by: Sainan <sainan@calamity.gg>
Co-committed-by: Sainan <sainan@calamity.gg>
2025-06-01 03:12:07 -07:00
28da982c80 feat: renamePet (#2106)
Reviewed-on: OpenWF/SpaceNinjaServer#2106
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-30 11:42:45 -07:00
ae1850d6cd chore: update PE+ (#2105)
Reviewed-on: OpenWF/SpaceNinjaServer#2105
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-27 03:58:12 -07:00
b90bdd2783 chore: update french translation (#2104)
Reviewed-on: OpenWF/SpaceNinjaServer#2104
Co-authored-by: Vitruvio <vitruvio@noreply.localhost>
Co-committed-by: Vitruvio <vitruvio@noreply.localhost>
2025-05-26 01:46:19 -07:00
225 changed files with 16421 additions and 11069 deletions

View File

@ -1,36 +1,46 @@
{ {
"plugins": ["@typescript-eslint", "prettier", "import"],
"extends": [ "extends": [
"eslint:recommended", "eslint:recommended",
"plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking" "plugin:@typescript-eslint/recommended-requiring-type-checking",
"plugin:import/recommended",
"plugin:import/typescript"
], ],
"plugins": ["@typescript-eslint", "prettier"],
"env": { "env": {
"browser": true, "browser": true,
"es6": true, "es6": true,
"node": true "node": true
}, },
"rules": { "rules": {
"@typescript-eslint/explicit-function-return-type": "warn", "@typescript-eslint/explicit-function-return-type": "error",
"@typescript-eslint/restrict-template-expressions": "warn", "@typescript-eslint/restrict-template-expressions": "error",
"@typescript-eslint/restrict-plus-operands": "warn", "@typescript-eslint/restrict-plus-operands": "error",
"@typescript-eslint/no-unsafe-member-access": "warn", "@typescript-eslint/no-unsafe-member-access": "error",
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "caughtErrors": "none" }], "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "caughtErrors": "none" }],
"@typescript-eslint/no-unsafe-argument": "error", "@typescript-eslint/no-unsafe-argument": "error",
"@typescript-eslint/no-unsafe-call": "warn", "@typescript-eslint/no-unsafe-call": "error",
"@typescript-eslint/no-unsafe-assignment": "warn", "@typescript-eslint/no-unsafe-assignment": "error",
"@typescript-eslint/no-explicit-any": "warn", "@typescript-eslint/no-explicit-any": "off",
"no-loss-of-precision": "warn", "no-loss-of-precision": "error",
"@typescript-eslint/no-unnecessary-condition": "warn", "@typescript-eslint/no-unnecessary-condition": "error",
"@typescript-eslint/no-base-to-string": "off", "@typescript-eslint/no-base-to-string": "off",
"no-case-declarations": "error", "no-case-declarations": "error",
"prettier/prettier": "error", "prettier/prettier": "error",
"no-mixed-spaces-and-tabs": "error", "no-mixed-spaces-and-tabs": "error",
"require-await": "off", "@typescript-eslint/require-await": "error",
"@typescript-eslint/require-await": "error" "import/no-named-as-default-member": "off",
"import/no-cycle": "warn"
}, },
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
"parserOptions": { "parserOptions": {
"project": "./tsconfig.json" "project": "./tsconfig.json"
},
"settings": {
"import/extensions": [ ".ts" ],
"import/resolver": {
"typescript": true,
"node": true
}
} }
} }

View File

@ -1,6 +1,7 @@
name: Build name: Build
on: on:
push: {} push:
branches: ["main"]
pull_request: {} pull_request: {}
jobs: jobs:
build: build:
@ -13,11 +14,12 @@ jobs:
with: with:
node-version: ">=20.6.0" node-version: ">=20.6.0"
- run: npm ci - run: npm ci
- run: cp config.json.example config.json - run: cp config-vanilla.json config.json
- run: npm run verify - run: npm run verify
- run: npm run lint:ci - run: npm run lint:ci
- run: npm run prettier - run: npm run prettier
- run: npm run update-translations - run: npm run update-translations
- run: npm run fix-imports
- name: Fail if there are uncommitted changes - name: Fail if there are uncommitted changes
run: | run: |
if [[ -n "$(git status --porcelain)" ]]; then if [[ -n "$(git status --porcelain)" ]]; then

View File

@ -4,9 +4,9 @@ on:
branches: branches:
- main - main
jobs: jobs:
docker: docker-amd64:
if: github.repository == 'OpenWF/SpaceNinjaServer' if: github.repository == 'OpenWF/SpaceNinjaServer'
runs-on: ubuntu-latest runs-on: amd64
steps: steps:
- name: Set up Docker buildx - name: Set up Docker buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
@ -18,8 +18,27 @@ jobs:
- name: Build and push - name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
platforms: linux/amd64,linux/arm64 platforms: linux/amd64
push: true push: true
tags: | tags: |
openwf/spaceninjaserver:latest openwf/spaceninjaserver:latest
openwf/spaceninjaserver:${{ github.sha }} openwf/spaceninjaserver:${{ github.sha }}
docker-arm64:
if: github.repository == 'OpenWF/SpaceNinjaServer'
runs-on: arm64
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/arm64
push: true
tags: |
openwf/spaceninjaserver:latest-arm64
openwf/spaceninjaserver:${{ github.sha }}-arm64

View File

@ -2,3 +2,4 @@ src/routes/api.ts
static/webui/libs/ static/webui/libs/
*.html *.html
*.md *.md
config-vanilla.json

3
.vscode/launch.json vendored
View File

@ -8,8 +8,7 @@
"type": "node", "type": "node",
"request": "launch", "request": "launch",
"name": "Debug and Watch", "name": "Debug and Watch",
"runtimeArgs": ["-r", "tsconfig-paths/register", "-r", "ts-node/register", "--watch-path", "src"], "args": ["${workspaceFolder}/scripts/dev.js"],
"args": ["${workspaceFolder}/src/index.ts"],
"console": "integratedTerminal" "console": "integratedTerminal"
} }
] ]

17
AGENTS.md Normal file
View File

@ -0,0 +1,17 @@
## In General
### Prerequisites
Use `npm i` or `npm ci` to install all dependencies.
### Testing
Use `npm run verify` to verify that your changes pass TypeScript's checks.
### Formatting
Use `npm run prettier` to ensure your formatting matches the expected format. Failing to do so will cause CI failure.
## WebUI Specific
The translation system is designed around additions being made to `static/webui/translations/en.js`. They are copied over for translation via `npm run update-translations`. DO NOT produce non-English strings; we want them to be translated by humans who can understand the full context.

View File

@ -1,53 +1,12 @@
FROM node:18-alpine3.19 FROM node:24-alpine3.21
ENV APP_MONGODB_URL=mongodb://mongodb:27017/openWF RUN apk add --no-cache bash jq
ENV APP_MY_ADDRESS=localhost
ENV APP_HTTP_PORT=80
ENV APP_HTTPS_PORT=443
ENV APP_AUTO_CREATE_ACCOUNT=true
ENV APP_SKIP_TUTORIAL=false
ENV APP_SKIP_ALL_DIALOGUE=false
ENV APP_UNLOCK_ALL_SCANS=false
ENV APP_UNLOCK_ALL_MISSIONS=false
ENV APP_INFINITE_CREDITS=false
ENV APP_INFINITE_PLATINUM=false
ENV APP_INFINITE_ENDO=false
ENV APP_INFINITE_REGAL_AYA=false
ENV APP_INFINITE_HELMINTH_MATERIALS=false
ENV APP_CLAIMING_BLUEPRINT_REFUNDS_INGREDIENTS=false
ENV APP_DONT_SUBTRACT_VOIDTRACES=false
ENV APP_DONT_SUBTRACT_CONSUMABLES=false
ENV APP_UNLOCK_ALL_SHIP_FEATURES=false
ENV APP_UNLOCK_ALL_SHIP_DECORATIONS=false
ENV APP_UNLOCK_ALL_FLAVOUR_ITEMS=false
ENV APP_UNLOCK_ALL_SKINS=false
ENV APP_UNLOCK_ALL_CAPTURA_SCENES=false
ENV APP_UNIVERSAL_POLARITY_EVERYWHERE=false
ENV APP_UNLOCK_DOUBLE_CAPACITY_POTATOES_EVERYWHERE=false
ENV APP_UNLOCK_EXILUS_EVERYWHERE=false
ENV APP_UNLOCK_ARCANES_EVERYWHERE=false
ENV APP_NO_DAILY_FOCUS_LIMIT=false
ENV APP_NO_ARGON_CRYSTAL_DECAY=false
ENV APP_NO_MASTERY_RANK_UP_COOLDOWN=false
ENV APP_NO_VENDOR_PURCHASE_LIMITS=true
ENV APP_NO_DEATH_MARKS=false
ENV APP_NO_KIM_COOLDOWNS=false
ENV APP_SYNDICATE_MISSIONS_REPEATABLE=false
ENV APP_INSTANT_FINISH_RIVEN_CHALLENGE=false
ENV APP_INSTANT_RESOURCE_EXTRACTOR_DRONES=false
ENV APP_NO_RESOURCE_EXTRACTOR_DRONES_DAMAGE=false
ENV APP_SKIP_CLAN_KEY_CRAFTING=false
ENV APP_NO_DOJO_ROOM_BUILD_STAGE=false
ENV APP_NO_DECO_BUILD_STAGE=false
ENV APP_FAST_DOJO_ROOM_DESTRUCTION=false
ENV APP_NO_DOJO_RESEARCH_COSTS=false
ENV APP_NO_DOJO_RESEARCH_TIME=false
ENV APP_FAST_CLAN_ASCENSION=false
ENV APP_SPOOF_MASTERY_RANK=-1
RUN apk add --no-cache bash sed wget jq
COPY . /app COPY . /app
WORKDIR /app WORKDIR /app
RUN npm i --omit=dev
RUN npm run build
RUN date '+%d %B %Y' > BUILD_DATE
ENTRYPOINT ["/app/docker-entrypoint.sh"] ENTRYPOINT ["/app/docker-entrypoint.sh"]

View File

@ -10,8 +10,29 @@ To get an idea of what functionality you can expect to be missing [have a look t
## config.json ## config.json
SpaceNinjaServer requires a `config.json`. To set it up, you can copy the [config.json.example](config.json.example), which has most cheats disabled. SpaceNinjaServer requires a `config.json`. To set it up, you can copy the [config-vanilla.json](config-vanilla.json), which has most cheats disabled.
- `logger.level` can be `fatal`, `error`, `warn`, `info`, `http`, `debug`, or `trace`. - `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 ]`. - `myIrcAddresses` can be used to point to an IRC server. If not provided, defaults to `[ myAddress ]`.
- `worldState.lockTime` will lock the time provided in worldState if nonzero, e.g. `1743202800` for night in POE. - `worldState.eidolonOverride` can be set to `day` or `night` to lock the time to day/fass and night/vome on Plains of Eidolon/Cambion Drift.
- `worldState.vallisOverride` can be set to `warm` or `cold` to lock the temperature on Orb Vallis.
- `worldState.duviriOverride` can be set to `joy`, `anger`, `envy`, `sorrow`, or `fear` to lock the Duviri spiral.
- `worldState.nightwaveOverride` will lock the nightwave season, assuming the client is new enough for it. Valid values:
- `RadioLegionIntermission13Syndicate` for Nora's Mix Vol. 9
- `RadioLegionIntermission12Syndicate` for Nora's Mix Vol. 8
- `RadioLegionIntermission11Syndicate` for Nora's Mix Vol. 7
- `RadioLegionIntermission10Syndicate` for Nora's Mix Vol. 6
- `RadioLegionIntermission9Syndicate` for Nora's Mix Vol. 5
- `RadioLegionIntermission8Syndicate` for Nora's Mix Vol. 4
- `RadioLegionIntermission7Syndicate` for Nora's Mix Vol. 3
- `RadioLegionIntermission6Syndicate` for Nora's Mix Vol. 2
- `RadioLegionIntermission5Syndicate` for Nora's Mix Vol. 1
- `RadioLegionIntermission4Syndicate` for Nora's Choice
- `RadioLegionIntermission3Syndicate` for Intermission III
- `RadioLegion3Syndicate` for Glassmaker
- `RadioLegionIntermission2Syndicate` for Intermission II
- `RadioLegion2Syndicate` for The Emissary
- `RadioLegionIntermissionSyndicate` for Intermission I
- `RadioLegionSyndicate` for The Wolf of Saturn Six
- `allTheFissures` can be set to `normal` or `hard` to enable all fissures either in normal or steel path, respectively.
- `worldState.circuitGameModes` can be set to an array of game modes which will override the otherwise-random pattern in The Circuit. Valid element values are `Survival`, `VoidFlood`, `Excavation`, `Defense`, `Exterminate`, `Assassination`, and `Alchemy`.

View File

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

23
UPDATE AND START SERVER.sh Executable file
View File

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

View File

@ -13,13 +13,16 @@
"skipTutorial": false, "skipTutorial": false,
"skipAllDialogue": false, "skipAllDialogue": false,
"unlockAllScans": false, "unlockAllScans": false,
"unlockAllMissions": false,
"infiniteCredits": false, "infiniteCredits": false,
"infinitePlatinum": false, "infinitePlatinum": false,
"infiniteEndo": false, "infiniteEndo": false,
"infiniteRegalAya": false, "infiniteRegalAya": false,
"infiniteHelminthMaterials": false, "infiniteHelminthMaterials": false,
"claimingBlueprintRefundsIngredients": false, "claimingBlueprintRefundsIngredients": false,
"dontSubtractPurchaseCreditCost": false,
"dontSubtractPurchasePlatinumCost": false,
"dontSubtractPurchaseItemCost": false,
"dontSubtractPurchaseStandingCost": false,
"dontSubtractVoidTraces": false, "dontSubtractVoidTraces": false,
"dontSubtractConsumables": false, "dontSubtractConsumables": false,
"unlockAllShipFeatures": false, "unlockAllShipFeatures": false,
@ -35,10 +38,14 @@
"noDailyFocusLimit": false, "noDailyFocusLimit": false,
"noArgonCrystalDecay": false, "noArgonCrystalDecay": false,
"noMasteryRankUpCooldown": false, "noMasteryRankUpCooldown": false,
"noVendorPurchaseLimits": true, "noVendorPurchaseLimits": false,
"noDeathMarks": false, "noDeathMarks": false,
"noKimCooldowns": false, "noKimCooldowns": false,
"fullyStockedVendors": false,
"baroAlwaysAvailable": false,
"baroFullyStocked": false,
"syndicateMissionsRepeatable": false, "syndicateMissionsRepeatable": false,
"unlockAllProfitTakerStages": false,
"instantFinishRivenChallenge": false, "instantFinishRivenChallenge": false,
"instantResourceExtractorDrones": false, "instantResourceExtractorDrones": false,
"noResourceExtractorDronesDamage": false, "noResourceExtractorDronesDamage": false,
@ -49,12 +56,45 @@
"noDojoResearchCosts": false, "noDojoResearchCosts": false,
"noDojoResearchTime": false, "noDojoResearchTime": false,
"fastClanAscension": false, "fastClanAscension": false,
"missionsCanGiveAllRelics": false,
"unlockAllSimarisResearchEntries": false,
"disableDailyTribute": false,
"spoofMasteryRank": -1, "spoofMasteryRank": -1,
"relicRewardItemCountMultiplier": 1,
"nightwaveStandingMultiplier": 1,
"unfaithfulBugFixes": {
"ignore1999LastRegionPlayed": false,
"fixXtraCheeseTimer": false
},
"worldState": { "worldState": {
"creditBoost": false, "creditBoost": false,
"affinityBoost": false, "affinityBoost": false,
"resourceBoost": false, "resourceBoost": false,
"starDays": true, "tennoLiveRelay": false,
"lockTime": 0 "wolfHunt": false,
"longShadow": false,
"hallowedFlame": false,
"hallowedNightmares": false,
"hallowedNightmaresRewardsOverride": 0,
"proxyRebellion": false,
"proxyRebellionRewardsOverride": 0,
"galleonOfGhouls": 0,
"ghoulEmergenceOverride": null,
"plagueStarOverride": null,
"starDaysOverride": null,
"dogDaysOverride": null,
"dogDaysRewardsOverride": null,
"eidolonOverride": "",
"vallisOverride": "",
"duviriOverride": "",
"nightwaveOverride": "",
"allTheFissures": "",
"circuitGameModes": null,
"darvoStockMultiplier": 1,
"varziaOverride": "",
"varziaFullyStocked": false
},
"dev": {
"keepVendorsExpired": false
} }
} }

View File

@ -1,62 +1,20 @@
services: services:
spaceninjaserver: spaceninjaserver:
# build: . # The image to use. If you have an ARM CPU, replace 'latest' with 'latest-arm64'.
image: openwf/spaceninjaserver:latest image: openwf/spaceninjaserver:latest
environment:
APP_MONGODB_URL: mongodb://openwfagent:spaceninjaserver@mongodb:27017/
# Following environment variables are set to default image values.
# Uncomment to edit.
# APP_MY_ADDRESS: localhost
# APP_HTTP_PORT: 80
# APP_HTTPS_PORT: 443
# APP_AUTO_CREATE_ACCOUNT: true
# APP_SKIP_TUTORIAL: false
# APP_SKIP_ALL_DIALOGUE: false
# APP_UNLOCK_ALL_SCANS: false
# APP_UNLOCK_ALL_MISSIONS: false
# APP_INFINITE_CREDITS: false
# APP_INFINITE_PLATINUM: false
# APP_INFINITE_ENDO: false
# APP_INFINITE_REGAL_AYA: false
# APP_INFINITE_HELMINTH_MATERIALS: false
# APP_CLAIMING_BLUEPRINT_REFUNDS_INGREDIENTS: false
# APP_DONT_SUBTRACT_VOIDTRACES: false
# APP_DONT_SUBTRACT_CONSUMABLES: false
# APP_UNLOCK_ALL_SHIP_FEATURES: false
# APP_UNLOCK_ALL_SHIP_DECORATIONS: false
# APP_UNLOCK_ALL_FLAVOUR_ITEMS: false
# APP_UNLOCK_ALL_SKINS: false
# APP_UNLOCK_ALL_CAPTURA_SCENES: false
# APP_UNIVERSAL_POLARITY_EVERYWHERE: false
# APP_UNLOCK_DOUBLE_CAPACITY_POTATOES_EVERYWHERE: false
# APP_UNLOCK_EXILUS_EVERYWHERE: false
# APP_UNLOCK_ARCANES_EVERYWHERE: false
# APP_NO_DAILY_FOCUS_LIMIT: false
# APP_NO_ARGON_CRYSTAL_DECAY: false
# APP_NO_MASTERY_RANK_UP_COOLDOWN: false
# APP_NO_VENDOR_PURCHASE_LIMITS: true
# APP_NO_DEATH_MARKS: false
# APP_NO_KIM_COOLDOWNS: false
# APP_SYNDICATE_MISSIONS_REPEATABLE: false
# APP_INSTANT_FINISH_RIVEN_CHALLENGE: false
# APP_INSTANT_RESOURCE_EXTRACTOR_DRONES: false
# APP_NO_RESOURCE_EXTRACTOR_DRONES_DAMAGE: false
# APP_SKIP_CLAN_KEY_CRAFTING: false
# APP_NO_DOJO_ROOM_BUILD_STAGE: false
# APP_NO_DECO_BUILD_STAGE: false
# APP_FAST_DOJO_ROOM_DESTRUCTION: false
# APP_NO_DOJO_RESEARCH_COSTS: false
# APP_NO_DOJO_RESEARCH_TIME: false
# APP_FAST_CLAN_ASCENSION: false
# APP_SPOOF_MASTERY_RANK: -1
volumes: volumes:
- ./docker-data/static:/app/static/data - ./docker-data/conf:/app/conf
- ./docker-data/static-data:/app/static/data
- ./docker-data/logs:/app/logs - ./docker-data/logs:/app/logs
ports: ports:
- 80:80 - 80:80
- 443:443 - 443:443
# Normally, the image is fetched from Docker Hub, but you can use the local Dockerfile by removing "image" above and adding this:
#build: .
# Works best when using `docker-compose up --force-recreate --build`.
depends_on: depends_on:
- mongodb - mongodb
mongodb: mongodb:
@ -66,3 +24,4 @@ services:
MONGO_INITDB_ROOT_PASSWORD: spaceninjaserver MONGO_INITDB_ROOT_PASSWORD: spaceninjaserver
volumes: volumes:
- ./docker-data/database:/data/db - ./docker-data/database:/data/db
command: mongod --quiet --logpath /dev/null

View File

@ -1,24 +1,8 @@
#!/bin/bash #!/bin/bash
set -e set -e
# Set up the configuration file using environment variables. if [ ! -f conf/config.json ]; then
echo '{ jq --arg value "mongodb://openwfagent:spaceninjaserver@mongodb:27017/" '.mongodbUrl = $value' /app/config-vanilla.json > /app/conf/config.json
"logger": { fi
"files": true,
"level": "trace",
"__valid_levels": "fatal, error, warn, info, http, debug, trace"
}
}
' > config.json
for config in $(env | grep "APP_") exec npm run start -- --configPath conf/config.json
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 i --omit=dev
npm run build
exec npm run start

3005
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,19 +5,33 @@
"main": "index.ts", "main": "index.ts",
"scripts": { "scripts": {
"start": "node --enable-source-maps --import ./build/src/pathman.js build/src/index.js", "start": "node --enable-source-maps --import ./build/src/pathman.js build/src/index.js",
"dev": "ts-node-dev --openssl-legacy-provider -r tsconfig-paths/register src/index.ts ", "build": "tsgo --sourceMap && ncp static/webui build/static/webui",
"build": "tsc --incremental --sourceMap && ncp static/webui build/static/webui", "build:tsc": "tsc --incremental --sourceMap && ncp static/webui build/static/webui",
"build:dev": "tsgo --sourceMap",
"build:dev:tsc": "tsc --incremental --sourceMap",
"build-and-start": "npm run build && npm run start",
"build-and-start:bun": "npm run verify && npm run bun-run",
"dev": "node scripts/dev.js",
"dev:bun": "bun scripts/dev.js",
"verify": "tsgo --noEmit", "verify": "tsgo --noEmit",
"verify:tsc": "tsc --noEmit",
"bun-run": "bun src/index.ts",
"lint": "eslint --ext .ts .", "lint": "eslint --ext .ts .",
"lint:ci": "eslint --ext .ts --rule \"prettier/prettier: off\" .", "lint:ci": "eslint --ext .ts --rule \"prettier/prettier: off\" .",
"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" "update-translations": "cd scripts && node update-translations.js",
"fix-imports": "cd scripts && node fix-imports.js",
"fix": "npm run update-translations && npm run fix-imports && npm run prettier"
}, },
"license": "GNU", "license": "GNU",
"dependencies": { "dependencies": {
"@types/express": "^5", "@types/express": "^5",
"@types/morgan": "^1.9.9", "@types/morgan": "^1.9.9",
"@types/websocket": "^1.0.10",
"@types/ws": "^8.18.1",
"@typescript/native-preview": "^7.0.0-dev.20250625.1",
"chokidar": "^4.0.3",
"crc-32": "^1.2.2", "crc-32": "^1.2.2",
"express": "^5", "express": "^5",
"json-with-bigint": "^3.4.4", "json-with-bigint": "^3.4.4",
@ -25,19 +39,21 @@
"morgan": "^1.10.0", "morgan": "^1.10.0",
"ncp": "^2.0.0", "ncp": "^2.0.0",
"typescript": "^5.5", "typescript": "^5.5",
"warframe-public-export-plus": "^0.5.64", "undici": "^7.10.0",
"warframe-public-export-plus": "^0.5.80",
"warframe-riven-info": "^0.1.2", "warframe-riven-info": "^0.1.2",
"winston": "^3.17.0", "winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0" "winston-daily-rotate-file": "^5.0.0",
"ws": "^8.18.2"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "^8.28.0", "@typescript-eslint/eslint-plugin": "^8.28.0",
"@typescript-eslint/parser": "^8.28.0", "@typescript-eslint/parser": "^8.28.0",
"@typescript/native-preview": "^7.0.0-dev.20250523.1",
"eslint": "^8", "eslint": "^8",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-prettier": "^5.2.5", "eslint-plugin-prettier": "^5.2.5",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"ts-node-dev": "^2.0.0", "tree-kill": "^1.2.2"
"tsconfig-paths": "^4.2.0"
} }
} }

58
scripts/dev.js Normal file
View File

@ -0,0 +1,58 @@
/* eslint-disable */
const { spawn } = require("child_process");
const chokidar = require("chokidar");
const kill = require("tree-kill");
let secret = "";
for (let i = 0; i != 10; ++i) {
secret += String.fromCharCode(Math.floor(Math.random() * 26) + 0x41);
}
const args = [...process.argv].splice(2);
args.push("--dev");
args.push("--secret");
args.push(secret);
let buildproc, runproc;
const spawnopts = { stdio: "inherit", shell: true };
function run(changedFile) {
if (changedFile) {
console.log(`Change to ${changedFile} detected`);
}
if (buildproc) {
kill(buildproc.pid);
buildproc = undefined;
}
if (runproc) {
kill(runproc.pid);
runproc = undefined;
}
const thisbuildproc = spawn("npm", ["run", process.versions.bun ? "verify" : "build:dev"], spawnopts);
const thisbuildstart = Date.now();
buildproc = thisbuildproc;
buildproc.on("exit", code => {
if (buildproc !== thisbuildproc) {
return;
}
buildproc = undefined;
if (code === 0) {
console.log(`${process.versions.bun ? "Verified" : "Built"} in ${Date.now() - thisbuildstart} ms`);
runproc = spawn("npm", ["run", process.versions.bun ? "bun-run" : "start", "--", ...args], spawnopts);
runproc.on("exit", () => {
runproc = undefined;
});
}
});
}
run();
chokidar.watch("src").on("change", run);
chokidar.watch("static/fixed_responses").on("change", run);
chokidar.watch("static/webui").on("change", async () => {
try {
await fetch("http://localhost/custom/webuiFileChangeDetected?secret=" + secret);
} catch (e) {}
});

46
scripts/fix-imports.js Normal file
View File

@ -0,0 +1,46 @@
/* eslint-disable */
const fs = require("fs");
const path = require("path");
const root = path.join(process.cwd(), "..");
function listFiles(dir) {
const entries = fs.readdirSync(dir, { withFileTypes: true });
let results = [];
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
results = results.concat(listFiles(fullPath));
} else {
results.push(fullPath);
}
}
return results;
}
const files = listFiles(path.join(root, "src"));
for (const file of files) {
let content;
try {
content = fs.readFileSync(file, "utf8");
} catch (e) {
continue;
}
const dir = path.dirname(file);
const fixedContent = content.replaceAll(/} from "([^"]+)";/g, (sub, importPath) => {
if (!importPath.startsWith("@/")) {
const fullImportPath = path.resolve(dir, importPath);
if (fs.existsSync(fullImportPath + ".ts")) {
const relative = path.relative(root, fullImportPath).replace(/\\/g, "/");
const fixedPath = "@/" + relative;
console.log(`${importPath} -> ${fixedPath}`);
return sub.split(importPath).join(fixedPath);
}
}
return sub;
});
if (content != fixedContent) {
fs.writeFileSync(file, fixedContent, "utf8");
}
}

View File

@ -1,6 +1,7 @@
// Based on https://onlyg.it/OpenWF/Translations/src/branch/main/update.php // Based on https://onlyg.it/OpenWF/Translations/src/branch/main/update.php
// Converted via ChatGPT-4o // Converted via ChatGPT-4o
/* eslint-disable */
const fs = require("fs"); const fs = require("fs");
function extractStrings(content) { function extractStrings(content) {
@ -30,7 +31,7 @@ fs.readdirSync("../static/webui/translations").forEach(file => {
const strings = extractStrings(line); const strings = extractStrings(line);
if (Object.keys(strings).length > 0) { if (Object.keys(strings).length > 0) {
Object.entries(strings).forEach(([key, value]) => { Object.entries(strings).forEach(([key, value]) => {
if (targetStrings.hasOwnProperty(key)) { if (targetStrings.hasOwnProperty(key) && !targetStrings[key].startsWith("[UNTRANSLATED] ")) {
fs.writeSync(fileHandle, ` ${key}: \`${targetStrings[key]}\`,\n`); fs.writeSync(fileHandle, ` ${key}: \`${targetStrings[key]}\`,\n`);
} else { } else {
fs.writeSync(fileHandle, ` ${key}: \`[UNTRANSLATED] ${value}\`,\n`); fs.writeSync(fileHandle, ` ${key}: \`[UNTRANSLATED] ${value}\`,\n`);

View File

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

View File

@ -0,0 +1,22 @@
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getPersonalRooms } from "@/src/services/personalRoomsService";
import { RequestHandler } from "express";
export const apartmentController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const personalRooms = await getPersonalRooms(accountId, "Apartment");
const response: IApartmentResponse = {};
if (req.query.backdrop !== undefined) {
response.NewBackdropItem = personalRooms.Apartment.VideoWallBackdrop = req.query.backdrop as string;
}
if (req.query.soundscape !== undefined) {
response.NewSoundscapeItem = personalRooms.Apartment.Soundscape = req.query.soundscape as string;
}
await personalRooms.save();
res.json(response);
};
interface IApartmentResponse {
NewBackdropItem?: string;
NewSoundscapeItem?: string;
}

View File

@ -24,7 +24,6 @@ export const artifactsController: RequestHandler = async (req, res) => {
if (itemIndex !== -1) { if (itemIndex !== -1) {
Upgrades[itemIndex].UpgradeFingerprint = stringifiedUpgradeFingerprint; Upgrades[itemIndex].UpgradeFingerprint = stringifiedUpgradeFingerprint;
inventory.markModified(`Upgrades.${itemIndex}.UpgradeFingerprint`);
} else { } else {
itemIndex = itemIndex =
Upgrades.push({ Upgrades.push({

View File

@ -1,16 +1,12 @@
import { getAccountForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
const checkDailyMissionBonusController: RequestHandler = (_req, res) => { export const checkDailyMissionBonusController: RequestHandler = async (req, res) => {
const data = Buffer.from([ const account = await getAccountForRequest(req);
0x44, 0x61, 0x69, 0x6c, 0x79, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x42, 0x6f, 0x6e, 0x75, 0x73, 0x3a, const today = Math.trunc(Date.now() / 86400000) * 86400;
0x31, 0x2d, 0x44, 0x61, 0x69, 0x6c, 0x79, 0x50, 0x56, 0x50, 0x57, 0x69, 0x6e, 0x42, 0x6f, 0x6e, 0x75, 0x73, if (account.DailyFirstWinDate != today) {
0x3a, 0x31, 0x0a res.send("DailyMissionBonus:1-DailyPVPWinBonus:1\n");
]); } else {
res.writeHead(200, { res.send("DailyMissionBonus:0-DailyPVPWinBonus:1\n");
"Content-Type": "text/html", }
"Content-Length": data.length
});
res.end(data);
}; };
export { checkDailyMissionBonusController };

View File

@ -13,15 +13,18 @@ import {
addItem, addItem,
addRecipes, addRecipes,
occupySlot, occupySlot,
combineInventoryChanges combineInventoryChanges,
addKubrowPetPrint,
addPowerSuit,
addEquipment
} from "@/src/services/inventoryService"; } from "@/src/services/inventoryService";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { InventorySlot, IPendingRecipeDatabase } from "@/src/types/inventoryTypes/inventoryTypes"; import { InventorySlot, IPendingRecipeDatabase } from "@/src/types/inventoryTypes/inventoryTypes";
import { toOid2 } from "@/src/helpers/inventoryHelpers"; import { toOid2 } from "@/src/helpers/inventoryHelpers";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { IRecipe } from "warframe-public-export-plus"; import { IRecipe } from "warframe-public-export-plus";
import { config } from "@/src/services/configService"; import { config } from "@/src/services/configService";
import { EquipmentFeatures, IEquipmentClient, Status } from "@/src/types/equipmentTypes";
interface IClaimCompletedRecipeRequest { interface IClaimCompletedRecipeRequest {
RecipeIds: IOid[]; RecipeIds: IOid[];
@ -105,20 +108,145 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
...updateCurrency(inventory, cost, true) ...updateCurrency(inventory, cost, true)
}; };
} }
if (recipe.secretIngredientAction != "SIA_UNBRAND") {
InventoryChanges = { if (recipe.secretIngredientAction == "SIA_CREATE_KUBROW") {
...InventoryChanges, const pet = inventory.KubrowPets.id(pendingRecipe.KubrowPet!)!;
...(await addItem( if (pet.Details!.HatchDate!.getTime() > Date.now()) {
pet.Details!.HatchDate = new Date();
}
let canSetActive = true;
for (const pet of inventory.KubrowPets) {
if (pet.Details!.Status == Status.StatusAvailable) {
canSetActive = false;
break;
}
}
pet.Details!.Status = canSetActive ? Status.StatusAvailable : Status.StatusStasis;
} else if (recipe.secretIngredientAction == "SIA_DISTILL_PRINT") {
const pet = inventory.KubrowPets.id(pendingRecipe.KubrowPet!)!;
addKubrowPetPrint(inventory, pet, InventoryChanges);
} else if (recipe.secretIngredientAction != "SIA_UNBRAND") {
if (recipe.resultType == "/Lotus/Powersuits/Excalibur/ExcaliburUmbra") {
// Quite the special case here...
// We don't just get Umbra, but also Skiajati and Umbra Mods. Both items are max rank, potatoed, and with the mods are pre-installed.
// Source: https://wiki.warframe.com/w/The_Sacrifice, https://wiki.warframe.com/w/Excalibur/Umbra, https://wiki.warframe.com/w/Skiajati
const umbraModA = (
await addItem(
inventory,
"/Lotus/Upgrades/Mods/Sets/Umbra/WarframeUmbraModA",
1,
false,
undefined,
`{"lvl":5}`
)
).Upgrades![0];
const umbraModB = (
await addItem(
inventory,
"/Lotus/Upgrades/Mods/Sets/Umbra/WarframeUmbraModB",
1,
false,
undefined,
`{"lvl":5}`
)
).Upgrades![0];
const umbraModC = (
await addItem(
inventory,
"/Lotus/Upgrades/Mods/Sets/Umbra/WarframeUmbraModC",
1,
false,
undefined,
`{"lvl":5}`
)
).Upgrades![0];
const sacrificeModA = (
await addItem(
inventory,
"/Lotus/Upgrades/Mods/Sets/Sacrifice/MeleeSacrificeModA",
1,
false,
undefined,
`{"lvl":5}`
)
).Upgrades![0];
const sacrificeModB = (
await addItem(
inventory,
"/Lotus/Upgrades/Mods/Sets/Sacrifice/MeleeSacrificeModB",
1,
false,
undefined,
`{"lvl":5}`
)
).Upgrades![0];
InventoryChanges.Upgrades ??= [];
InventoryChanges.Upgrades.push(umbraModA, umbraModB, umbraModC, sacrificeModA, sacrificeModB);
await addPowerSuit(
inventory, inventory,
recipe.resultType, "/Lotus/Powersuits/Excalibur/ExcaliburUmbra",
recipe.num, {
false, Configs: [
undefined, {
pendingRecipe.TargetFingerprint Upgrades: [
)) "",
}; "",
"",
"",
"",
umbraModA.ItemId.$oid,
umbraModB.ItemId.$oid,
umbraModC.ItemId.$oid
]
}
],
XP: 900_000,
Features: EquipmentFeatures.DOUBLE_CAPACITY
},
InventoryChanges
);
inventory.XPInfo.push({
ItemType: "/Lotus/Powersuits/Excalibur/ExcaliburUmbra",
XP: 900_000
});
addEquipment(
inventory,
"Melee",
"/Lotus/Weapons/Tenno/Melee/Swords/UmbraKatana/UmbraKatana",
{
Configs: [
{ Upgrades: ["", "", "", "", "", "", sacrificeModA.ItemId.$oid, sacrificeModB.ItemId.$oid] }
],
XP: 450_000,
Features: EquipmentFeatures.DOUBLE_CAPACITY
},
InventoryChanges
);
inventory.XPInfo.push({
ItemType: "/Lotus/Weapons/Tenno/Melee/Swords/UmbraKatana/UmbraKatana",
XP: 450_000
});
} else {
InventoryChanges = {
...InventoryChanges,
...(await addItem(
inventory,
recipe.resultType,
recipe.num,
false,
undefined,
pendingRecipe.TargetFingerprint
))
};
}
} }
if (config.claimingBlueprintRefundsIngredients) { if (
config.claimingBlueprintRefundsIngredients &&
recipe.secretIngredientAction != "SIA_CREATE_KUBROW" // Can't refund the egg
) {
await refundRecipeIngredients(inventory, InventoryChanges, recipe, pendingRecipe); await refundRecipeIngredients(inventory, InventoryChanges, recipe, pendingRecipe);
} }
await inventory.save(); await inventory.save();

View File

@ -0,0 +1,35 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { combineInventoryChanges, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { RequestHandler } from "express";
import { ExportChallenges } from "warframe-public-export-plus";
export const claimJunctionChallengeRewardController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const data = getJSONfromString<IClaimJunctionChallengeRewardRequest>(String(req.body));
const challengeProgress = inventory.ChallengeProgress.find(x => x.Name == data.Challenge)!;
if (challengeProgress.ReceivedJunctionReward) {
throw new Error(`attempt to double-claim junction reward`);
}
challengeProgress.ReceivedJunctionReward = true;
inventory.ClaimedJunctionChallengeRewards ??= [];
inventory.ClaimedJunctionChallengeRewards.push(data.Challenge);
const challengeMeta = Object.entries(ExportChallenges).find(arr => arr[0].endsWith("/" + data.Challenge))![1];
const inventoryChanges = {};
for (const reward of challengeMeta.countedRewards!) {
combineInventoryChanges(
inventoryChanges,
(await handleStoreItemAcquisition(reward.StoreItem, inventory, reward.ItemCount)).InventoryChanges
);
}
await inventory.save();
res.json({
inventoryChanges: inventoryChanges // Yeah, it's "inventoryChanges" in the response here.
});
};
interface IClaimJunctionChallengeRewardRequest {
Challenge: string;
}

View File

@ -1,4 +1,4 @@
import { getCalendarProgress, getInventory } from "@/src/services/inventoryService"; import { checkCalendarAutoAdvance, getCalendarProgress, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { getWorldState } from "@/src/services/worldStateService"; import { getWorldState } from "@/src/services/worldStateService";
@ -12,27 +12,23 @@ export const completeCalendarEventController: RequestHandler = async (req, res)
const calendarProgress = getCalendarProgress(inventory); const calendarProgress = getCalendarProgress(inventory);
const currentSeason = getWorldState().KnownCalendarSeasons[0]; const currentSeason = getWorldState().KnownCalendarSeasons[0];
let inventoryChanges: IInventoryChanges = {}; let inventoryChanges: IInventoryChanges = {};
let dayIndex = 0; const dayIndex = calendarProgress.SeasonProgress.LastCompletedDayIdx + 1;
for (const day of currentSeason.Days) { const day = currentSeason.Days[dayIndex];
if (day.events.length == 0 || day.events[0].type != "CET_CHALLENGE") { if (day.events.length != 0) {
if (dayIndex == calendarProgress.SeasonProgress.LastCompletedDayIdx) { if (day.events[0].type == "CET_CHALLENGE") {
if (day.events.length != 0) { throw new Error(`completeCalendarEvent should not be used for challenges`);
const selection = day.events[parseInt(req.query.CompletedEventIdx as string)]; }
if (selection.type == "CET_REWARD") { const selection = day.events[parseInt(req.query.CompletedEventIdx as string)];
inventoryChanges = (await handleStoreItemAcquisition(selection.reward!, inventory)) if (selection.type == "CET_REWARD") {
.InventoryChanges; inventoryChanges = (await handleStoreItemAcquisition(selection.reward!, inventory)).InventoryChanges;
} else if (selection.type == "CET_UPGRADE") { } else if (selection.type == "CET_UPGRADE") {
calendarProgress.YearProgress.Upgrades.push(selection.upgrade!); calendarProgress.YearProgress.Upgrades.push(selection.upgrade!);
} else if (selection.type != "CET_PLOT") { } else if (selection.type != "CET_PLOT") {
throw new Error(`unexpected selection type: ${selection.type}`); throw new Error(`unexpected selection type: ${selection.type}`);
}
}
break;
}
++dayIndex;
} }
} }
calendarProgress.SeasonProgress.LastCompletedDayIdx++; calendarProgress.SeasonProgress.LastCompletedDayIdx = dayIndex;
checkCalendarAutoAdvance(inventory, currentSeason);
await inventory.save(); await inventory.save();
res.json({ res.json({
InventoryChanges: inventoryChanges, InventoryChanges: inventoryChanges,

View File

@ -4,8 +4,7 @@ import { addMiscItems, getInventory, updateCurrency } from "@/src/services/inven
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { createUnveiledRivenFingerprint } from "@/src/helpers/rivenHelper"; import { IVeiledRivenFingerprint } from "@/src/helpers/rivenHelper";
import { ExportUpgrades } from "warframe-public-export-plus";
export const completeRandomModChallengeController: RequestHandler = async (req, res) => { export const completeRandomModChallengeController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
@ -27,10 +26,11 @@ export const completeRandomModChallengeController: RequestHandler = async (req,
inventoryChanges.MiscItems = miscItemChanges; inventoryChanges.MiscItems = miscItemChanges;
} }
// Update riven fingerprint to a randomised unveiled state // Complete the riven challenge
const upgrade = inventory.Upgrades.id(request.ItemId)!; const upgrade = inventory.Upgrades.id(request.ItemId)!;
const meta = ExportUpgrades[upgrade.ItemType]; const fp = JSON.parse(upgrade.UpgradeFingerprint!) as IVeiledRivenFingerprint;
upgrade.UpgradeFingerprint = JSON.stringify(createUnveiledRivenFingerprint(meta)); fp.challenge.Progress = fp.challenge.Required;
upgrade.UpgradeFingerprint = JSON.stringify(fp);
await inventory.save(); await inventory.save();

View File

@ -21,7 +21,8 @@ import {
updateCurrency updateCurrency
} from "@/src/services/inventoryService"; } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { IFusionTreasure, IMiscItem, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes"; import { ITypeCount } from "@/src/types/commonTypes";
import { IFusionTreasure, IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
export const contributeToVaultController: RequestHandler = async (req, res) => { export const contributeToVaultController: RequestHandler = async (req, res) => {

View File

@ -4,9 +4,15 @@ import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
export const creditsController: RequestHandler = async (req, res) => { export const creditsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const inventory = (
await Promise.all([
const inventory = await getInventory(accountId, "RegularCredits TradesRemaining PremiumCreditsFree PremiumCredits"); getAccountIdForRequest(req),
getInventory(
req.query.accountId as string,
"RegularCredits TradesRemaining PremiumCreditsFree PremiumCredits"
)
])
)[1];
const response = { const response = {
RegularCredits: inventory.RegularCredits, RegularCredits: inventory.RegularCredits,

View File

@ -0,0 +1,106 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { addMiscItems, freeUpSlot, getInventory, updateCurrency } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IOid } from "@/src/types/commonTypes";
import { ICrewShipComponentFingerprint, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { RequestHandler } from "express";
import { ExportCustoms, ExportDojoRecipes } from "warframe-public-export-plus";
export const crewShipFusionController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const payload = getJSONfromString<ICrewShipFusionRequest>(String(req.body));
const isWeapon = inventory.CrewShipWeapons.id(payload.PartA.$oid);
const itemA = isWeapon ?? inventory.CrewShipWeaponSkins.id(payload.PartA.$oid)!;
const category = isWeapon ? "CrewShipWeapons" : "CrewShipWeaponSkins";
const salvageCategory = isWeapon ? "CrewShipSalvagedWeapons" : "CrewShipSalvagedWeaponSkins";
const itemB = inventory[payload.SourceRecipe ? salvageCategory : category].id(payload.PartB.$oid)!;
const tierA = itemA.ItemType.charCodeAt(itemA.ItemType.length - 1) - 65;
const tierB = itemB.ItemType.charCodeAt(itemB.ItemType.length - 1) - 65;
const inventoryChanges: IInventoryChanges = {};
// Charge partial repair cost if fusing with an identified but unrepaired part
if (payload.SourceRecipe) {
const recipe = ExportDojoRecipes.research[payload.SourceRecipe];
updateCurrency(inventory, Math.round(recipe.price * 0.4), false, inventoryChanges);
const miscItemChanges = recipe.ingredients.map(x => ({ ...x, ItemCount: Math.round(x.ItemCount * -0.4) }));
addMiscItems(inventory, miscItemChanges);
inventoryChanges.MiscItems = miscItemChanges;
}
// Remove inferior item
if (payload.SourceRecipe) {
inventory[salvageCategory].pull({ _id: payload.PartB.$oid });
inventoryChanges.RemovedIdItems = [{ ItemId: payload.PartB }];
} else {
const inferiorId = tierA < tierB ? payload.PartA : payload.PartB;
inventory[category].pull({ _id: inferiorId.$oid });
inventoryChanges.RemovedIdItems = [{ ItemId: inferiorId }];
freeUpSlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS);
inventoryChanges[InventorySlot.RJ_COMPONENT_AND_ARMAMENTS] = { count: -1, platinum: 0, Slots: 1 };
}
// Upgrade superior item
const superiorItem = tierA < tierB ? itemB : itemA;
const inferiorItem = tierA < tierB ? itemA : itemB;
const fingerprint: ICrewShipComponentFingerprint = JSON.parse(
superiorItem.UpgradeFingerprint!
) as ICrewShipComponentFingerprint;
const inferiorFingerprint: ICrewShipComponentFingerprint = inferiorItem.UpgradeFingerprint
? (JSON.parse(inferiorItem.UpgradeFingerprint) as ICrewShipComponentFingerprint)
: { compat: "", buffs: [] };
if (isWeapon) {
for (let i = 0; i != fingerprint.buffs.length; ++i) {
const buffA = fingerprint.buffs[i];
const buffB = i < inferiorFingerprint.buffs.length ? inferiorFingerprint.buffs[i] : undefined;
const fvalA = buffA.Value / 0x3fffffff;
const fvalB = (buffB?.Value ?? 0) / 0x3fffffff;
const percA = 0.3 + fvalA * (0.6 - 0.3);
const percB = 0.3 + fvalB * (0.6 - 0.3);
const newPerc = Math.min(0.6, Math.max(percA, percB) * FUSE_MULTIPLIERS[Math.abs(tierA - tierB)]);
const newFval = (newPerc - 0.3) / (0.6 - 0.3);
buffA.Value = Math.trunc(newFval * 0x3fffffff);
}
} else {
const superiorMeta = ExportCustoms[superiorItem.ItemType].randomisedUpgrades ?? [];
const inferiorMeta = ExportCustoms[inferiorItem.ItemType].randomisedUpgrades ?? [];
for (let i = 0; i != inferiorFingerprint.buffs.length; ++i) {
const buffA = fingerprint.buffs[i];
const buffB = inferiorFingerprint.buffs[i];
const fvalA = buffA.Value / 0x3fffffff;
const fvalB = buffB.Value / 0x3fffffff;
const rangeA = superiorMeta[i].range;
const rangeB = inferiorMeta[i].range;
const percA = rangeA[0] + fvalA * (rangeA[1] - rangeA[0]);
const percB = rangeB[0] + fvalB * (rangeB[1] - rangeB[0]);
const newPerc = Math.min(rangeA[1], Math.max(percA, percB) * FUSE_MULTIPLIERS[Math.abs(tierA - tierB)]);
const newFval = (newPerc - rangeA[0]) / (rangeA[1] - rangeA[0]);
buffA.Value = Math.trunc(newFval * 0x3fffffff);
}
if (inferiorFingerprint.SubroutineIndex) {
const useSuperiorSubroutine = tierA < tierB ? !payload.UseSubroutineA : payload.UseSubroutineA;
if (!useSuperiorSubroutine) {
fingerprint.SubroutineIndex = inferiorFingerprint.SubroutineIndex;
}
}
}
superiorItem.UpgradeFingerprint = JSON.stringify(fingerprint);
inventoryChanges[category] = [superiorItem.toJSON() as any];
await inventory.save();
res.json({
InventoryChanges: inventoryChanges
});
};
interface ICrewShipFusionRequest {
PartA: IOid;
PartB: IOid;
SourceRecipe: string;
UseSubroutineA: boolean;
}
const FUSE_MULTIPLIERS = [1.1, 1.05, 1.02];

View File

@ -12,7 +12,7 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { getRandomInt } from "@/src/services/rngService"; import { getRandomInt } from "@/src/services/rngService";
import { IFingerprintStat } from "@/src/helpers/rivenHelper"; import { IFingerprintStat } from "@/src/helpers/rivenHelper";
import { IEquipmentDatabase } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { IEquipmentDatabase } from "@/src/types/equipmentTypes";
export const crewShipIdentifySalvageController: RequestHandler = async (req, res) => { export const crewShipIdentifySalvageController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);

View File

@ -1,12 +1,15 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Guild } from "@/src/models/guildModel"; import { Guild } from "@/src/models/guildModel";
import { getAccountForRequest } from "@/src/services/loginService"; import { hasAccessToDojo, hasGuildPermission } from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountForRequest, getAccountIdForRequest } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
export const customObstacleCourseLeaderboardController: RequestHandler = async (req, res) => { export const customObstacleCourseLeaderboardController: RequestHandler = async (req, res) => {
const data = getJSONfromString<ICustomObstacleCourseLeaderboardRequest>(String(req.body)); const data = getJSONfromString<ICustomObstacleCourseLeaderboardRequest>(String(req.body));
const guild = (await Guild.findById(data.g, "DojoComponents"))!; const guild = (await Guild.findById(data.g, "DojoComponents Ranks"))!;
const component = guild.DojoComponents.id(data.c)!; const component = guild.DojoComponents.id(data.c)!;
if (req.query.act == "f") { if (req.query.act == "f") {
res.json({ res.json({
@ -34,6 +37,19 @@ export const customObstacleCourseLeaderboardController: RequestHandler = async (
entry.r = ++r; entry.r = ++r;
} }
await guild.save(); await guild.save();
res.status(200).end();
} else if (req.query.act == "c") {
// TOVERIFY: What clan permission is actually needed for this?
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "GuildId LevelKeys");
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Decorator))) {
res.status(400).end();
return;
}
component.Leaderboard = undefined;
await guild.save();
res.status(200).end(); res.status(200).end();
} else { } else {
logger.debug(`data provided to ${req.path}: ${String(req.body)}`); logger.debug(`data provided to ${req.path}: ${String(req.body)}`);

View File

@ -3,11 +3,13 @@ import {
getGuildForRequestEx, getGuildForRequestEx,
hasAccessToDojo, hasAccessToDojo,
hasGuildPermission, hasGuildPermission,
refundDojoDeco,
removeDojoDeco removeDojoDeco
} from "@/src/services/guildService"; } from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes"; import { GuildPermission } from "@/src/types/guildTypes";
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
export const destroyDojoDecoController: RequestHandler = async (req, res) => { export const destroyDojoDecoController: RequestHandler = async (req, res) => {
@ -18,9 +20,20 @@ export const destroyDojoDecoController: RequestHandler = async (req, res) => {
res.json({ DojoRequestStatus: -1 }); res.json({ DojoRequestStatus: -1 });
return; return;
} }
const request = JSON.parse(String(req.body)) as IDestroyDojoDecoRequest; const request = JSON.parse(String(req.body)) as IDestroyDojoDecoRequest | IClearObstacleCourseRequest;
if ("DecoType" in request) {
removeDojoDeco(guild, request.ComponentId, request.DecoId); removeDojoDeco(guild, request.ComponentId, request.DecoId);
} else if (request.Act == "cObst") {
const component = guild.DojoComponents.id(request.ComponentId)!;
if (component.Decos) {
for (const deco of component.Decos) {
refundDojoDeco(guild, component, deco);
}
component.Decos.splice(0, component.Decos.length);
}
} else {
logger.error(`unhandled destroyDojoDeco request`, request);
}
await guild.save(); await guild.save();
res.json(await getDojoClient(guild, 0, request.ComponentId)); res.json(await getDojoClient(guild, 0, request.ComponentId));
@ -31,3 +44,8 @@ interface IDestroyDojoDecoRequest {
ComponentId: string; ComponentId: string;
DecoId: string; DecoId: string;
} }
interface IClearObstacleCourseRequest {
ComponentId: string;
Act: "cObst" | "maybesomethingelsewedontknowabout";
}

View File

@ -3,7 +3,7 @@ import { getAccountIdForRequest } from "@/src/services/loginService";
import { addMiscItems, 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 { getRecipe, WeaponTypeInternal } from "@/src/services/itemDataService"; import { getRecipe, WeaponTypeInternal } from "@/src/services/itemDataService";
import { EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { EquipmentFeatures } from "@/src/types/equipmentTypes";
export const evolveWeaponController: RequestHandler = async (req, res) => { export const evolveWeaponController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);

View File

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

View File

@ -4,7 +4,6 @@ import { getInventory, addMiscItems, addEquipment, occupySlot } from "@/src/serv
import { IMiscItem, TFocusPolarity, TEquipmentKey, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; import { IMiscItem, TFocusPolarity, TEquipmentKey, InventorySlot } 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";
import { Inventory } from "@/src/models/inventoryModels/inventoryModel"; import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
export const focusController: RequestHandler = async (req, res) => { export const focusController: RequestHandler = async (req, res) => {
@ -43,7 +42,7 @@ export const focusController: RequestHandler = async (req, res) => {
inventory.FocusAbility ??= focusType; inventory.FocusAbility ??= focusType;
inventory.FocusUpgrades.push({ ItemType: focusType }); inventory.FocusUpgrades.push({ ItemType: focusType });
if (inventory.FocusXP) { if (inventory.FocusXP) {
inventory.FocusXP[focusPolarity] -= cost; inventory.FocusXP[focusPolarity]! -= cost;
} }
await inventory.save(); await inventory.save();
res.json({ res.json({
@ -78,7 +77,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,
@ -96,7 +95,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,
@ -116,14 +115,14 @@ export const focusController: RequestHandler = async (req, res) => {
}); });
occupySlot(inventory, InventorySlot.AMPS, false); occupySlot(inventory, InventorySlot.AMPS, false);
await inventory.save(); await inventory.save();
res.json((inventoryChanges.OperatorAmps as IEquipmentClient[])[0]); res.json(inventoryChanges.OperatorAmps![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",
@ -168,8 +167,10 @@ export const focusController: RequestHandler = async (req, res) => {
shard.ItemCount *= -1; shard.ItemCount *= -1;
} }
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
inventory.FocusXP ??= { AP_POWER: 0, AP_TACTIC: 0, AP_DEFENSE: 0, AP_ATTACK: 0, AP_WARD: 0 }; const polarity = request.Polarity;
inventory.FocusXP[request.Polarity] += xp; inventory.FocusXP ??= {};
inventory.FocusXP[polarity] ??= 0;
inventory.FocusXP[polarity] += xp;
addMiscItems(inventory, request.Shards); addMiscItems(inventory, request.Shards);
await inventory.save(); await inventory.save();
break; break;

View File

@ -2,22 +2,14 @@ import { RequestHandler } from "express";
import { ExportResources } from "warframe-public-export-plus"; import { ExportResources } from "warframe-public-export-plus";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { addFusionTreasures, addMiscItems, getInventory } from "@/src/services/inventoryService"; import { addFusionTreasures, addMiscItems, getInventory } from "@/src/services/inventoryService";
import { IFusionTreasure, IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
import { parseFusionTreasure } from "@/src/helpers/inventoryHelpers";
interface IFusionTreasureRequest { interface IFusionTreasureRequest {
oldTreasureName: string; oldTreasureName: string;
newTreasureName: 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) => { export const fusionTreasuresController: 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

@ -6,9 +6,8 @@ import { getAccountIdForRequest } from "@/src/services/loginService";
import { createGarden, getPersonalRooms } from "@/src/services/personalRoomsService"; import { createGarden, getPersonalRooms } from "@/src/services/personalRoomsService";
import { IMongoDate } from "@/src/types/commonTypes"; import { IMongoDate } from "@/src/types/commonTypes";
import { IMissionReward } from "@/src/types/missionTypes"; import { IMissionReward } from "@/src/types/missionTypes";
import { IPersonalRoomsClient } from "@/src/types/personalRoomsTypes"; import { IGardeningClient, IPersonalRoomsClient } from "@/src/types/personalRoomsTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { IGardeningClient } from "@/src/types/shipTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { dict_en, ExportResources } from "warframe-public-export-plus"; import { dict_en, ExportResources } from "warframe-public-export-plus";

View File

@ -1,8 +1,10 @@
import { DailyDeal } from "@/src/models/worldStateModel";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
export const getDailyDealStockLevelsController: RequestHandler = (req, res) => { export const getDailyDealStockLevelsController: RequestHandler = async (req, res) => {
const dailyDeal = (await DailyDeal.findOne({ StoreItem: req.query.productName }, "AmountSold"))!;
res.json({ res.json({
StoreItem: req.query.productName, StoreItem: req.query.productName,
AmountSold: 0 AmountSold: dailyDeal.AmountSold
}); });
}; };

View File

@ -1,6 +1,6 @@
import { Inventory } from "@/src/models/inventoryModels/inventoryModel"; import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
import { generateRewardSeed } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { generateRewardSeed } from "@/src/services/rngService";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
export const getNewRewardSeedController: RequestHandler = async (req, res) => { export const getNewRewardSeedController: RequestHandler = async (req, res) => {

View File

@ -0,0 +1,62 @@
import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory } from "@/src/services/inventoryService";
import { EPOCH, getSeasonChallengePools, getWorldState, pushWeeklyActs } from "@/src/services/worldStateService";
import { unixTimesInMs } from "@/src/constants/timeConstants";
import { ISeasonChallenge } from "@/src/types/worldStateTypes";
import { ExportChallenges } from "warframe-public-export-plus";
export const getPastWeeklyChallengesController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "SeasonChallengeHistory ChallengeProgress");
const worldState = getWorldState(undefined);
if (worldState.SeasonInfo) {
const pools = getSeasonChallengePools(worldState.SeasonInfo.AffiliationTag);
const nightwaveStartTimestamp = Number(worldState.SeasonInfo.Activation.$date.$numberLong);
const nightwaveSeason = worldState.SeasonInfo.Season;
const timeMs = worldState.Time * 1000;
const completedChallengesIds = new Set<string>();
inventory.SeasonChallengeHistory.forEach(challengeHistory => {
const entryNightwaveSeason = parseInt(challengeHistory.id.slice(0, 4), 10) - 1;
if (nightwaveSeason == entryNightwaveSeason) {
const meta = Object.entries(ExportChallenges).find(
([key]) => key.split("/").pop() === challengeHistory.challenge
);
if (meta) {
const [, challengeMeta] = meta;
const challengeProgress = inventory.ChallengeProgress.find(
c => c.Name === challengeHistory.challenge
);
if (challengeProgress && challengeProgress.Progress >= (challengeMeta.requiredCount ?? 1)) {
completedChallengesIds.add(challengeHistory.id);
}
}
}
});
const PastWeeklyChallenges: ISeasonChallenge[] = [];
let week = Math.trunc((timeMs - EPOCH) / unixTimesInMs.week) - 1;
while (EPOCH + week * unixTimesInMs.week >= nightwaveStartTimestamp && PastWeeklyChallenges.length < 3) {
const tempActs: ISeasonChallenge[] = [];
pushWeeklyActs(tempActs, pools, week, nightwaveStartTimestamp, nightwaveSeason);
for (const act of tempActs) {
if (!completedChallengesIds.has(act._id.$oid) && PastWeeklyChallenges.length < 3) {
if (act.Challenge.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanent")) {
act.Permanent = true;
}
PastWeeklyChallenges.push(act);
}
}
week--;
}
res.json({ PastWeeklyChallenges: PastWeeklyChallenges });
}
};

View File

@ -3,10 +3,9 @@ import { config } from "@/src/services/configService";
import allShipFeatures from "@/static/fixed_responses/allShipFeatures.json"; import allShipFeatures from "@/static/fixed_responses/allShipFeatures.json";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { createGarden, getPersonalRooms } from "@/src/services/personalRoomsService"; import { createGarden, getPersonalRooms } from "@/src/services/personalRoomsService";
import { toOid } from "@/src/helpers/inventoryHelpers"; import { IGetShipResponse, IPersonalRoomsClient } from "@/src/types/personalRoomsTypes";
import { IGetShipResponse } from "@/src/types/shipTypes";
import { IPersonalRoomsClient } from "@/src/types/personalRoomsTypes";
import { getLoadout } from "@/src/services/loadoutService"; import { getLoadout } from "@/src/services/loadoutService";
import { toOid } from "@/src/helpers/inventoryHelpers";
export const getShipController: RequestHandler = async (req, res) => { export const getShipController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
@ -26,15 +25,7 @@ export const getShipController: RequestHandler = async (req, res) => {
LoadOutInventory: { LoadOutPresets: loadout.toJSON() }, LoadOutInventory: { LoadOutPresets: loadout.toJSON() },
Ship: { Ship: {
...personalRooms.Ship, ...personalRooms.Ship,
ShipId: toOid(personalRoomsDb.activeShipId), ShipId: toOid(personalRoomsDb.activeShipId)
ShipInterior: {
Colors: personalRooms.ShipInteriorColors,
ShipAttachments: { HOOD_ORNAMENT: "" },
SkinFlavourItem: ""
},
FavouriteLoadoutId: personalRooms.Ship.FavouriteLoadoutId
? toOid(personalRooms.Ship.FavouriteLoadoutId)
: undefined
}, },
Apartment: personalRooms.Apartment, Apartment: personalRooms.Apartment,
TailorShop: personalRooms.TailorShop TailorShop: personalRooms.TailorShop

View File

@ -2,6 +2,7 @@ import { RequestHandler } from "express";
import { applyStandingToVendorManifest, getVendorManifestByTypeName } from "@/src/services/serversideVendorsService"; import { applyStandingToVendorManifest, getVendorManifestByTypeName } from "@/src/services/serversideVendorsService";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { config } from "@/src/services/configService";
export const getVendorInfoController: RequestHandler = async (req, res) => { export const getVendorInfoController: RequestHandler = async (req, res) => {
let manifest = getVendorManifestByTypeName(req.query.vendor as string); let manifest = getVendorManifestByTypeName(req.query.vendor as string);
@ -14,6 +15,14 @@ export const getVendorInfoController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
manifest = applyStandingToVendorManifest(inventory, manifest); manifest = applyStandingToVendorManifest(inventory, manifest);
if (config.dev?.keepVendorsExpired) {
manifest = {
VendorInfo: {
...manifest.VendorInfo,
Expiry: { $date: { $numberLong: "0" } }
}
};
}
} }
res.json(manifest); res.json(manifest);

View File

@ -9,15 +9,26 @@ import {
updateCurrency updateCurrency
} from "@/src/services/inventoryService"; } from "@/src/services/inventoryService";
import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService"; import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; import { handleDailyDealPurchase, handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { IOid } from "@/src/types/commonTypes"; import { IOid } from "@/src/types/commonTypes";
import { IInventoryChanges, IPurchaseParams } from "@/src/types/purchaseTypes"; import { IPurchaseParams, IPurchaseResponse, PurchaseSource } from "@/src/types/purchaseTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { ExportBundles, ExportFlavour } from "warframe-public-export-plus"; import { ExportBundles, ExportFlavour } from "warframe-public-export-plus";
const checkPurchaseParams = (params: IPurchaseParams): boolean => {
switch (params.Source) {
case PurchaseSource.Market:
return params.UsePremium;
case PurchaseSource.DailyDeal:
return true;
}
return false;
};
export const giftingController: RequestHandler = async (req, res) => { export const giftingController: RequestHandler = async (req, res) => {
const data = getJSONfromString<IGiftingRequest>(String(req.body)); const data = getJSONfromString<IGiftingRequest>(String(req.body));
if (data.PurchaseParams.Source != 0 || !data.PurchaseParams.UsePremium) { if (!checkPurchaseParams(data.PurchaseParams)) {
throw new Error(`unexpected purchase params in gifting request: ${String(req.body)}`); throw new Error(`unexpected purchase params in gifting request: ${String(req.body)}`);
} }
@ -58,16 +69,19 @@ export const giftingController: RequestHandler = async (req, res) => {
} }
senderInventory.GiftsRemaining -= 1; senderInventory.GiftsRemaining -= 1;
const inventoryChanges: IInventoryChanges = updateCurrency( const response: IPurchaseResponse = {
senderInventory, InventoryChanges: {}
data.PurchaseParams.ExpectedPrice, };
true if (data.PurchaseParams.Source == PurchaseSource.DailyDeal) {
); await handleDailyDealPurchase(senderInventory, data.PurchaseParams, response);
} else {
updateCurrency(senderInventory, data.PurchaseParams.ExpectedPrice, true, response.InventoryChanges);
}
if (data.PurchaseParams.StoreItem in ExportBundles) { if (data.PurchaseParams.StoreItem in ExportBundles) {
const bundle = ExportBundles[data.PurchaseParams.StoreItem]; const bundle = ExportBundles[data.PurchaseParams.StoreItem];
if (bundle.giftingBonus) { if (bundle.giftingBonus) {
combineInventoryChanges( combineInventoryChanges(
inventoryChanges, response.InventoryChanges,
(await handleStoreItemAcquisition(bundle.giftingBonus, senderInventory)).InventoryChanges (await handleStoreItemAcquisition(bundle.giftingBonus, senderInventory)).InventoryChanges
); );
} }
@ -99,9 +113,7 @@ export const giftingController: RequestHandler = async (req, res) => {
} }
]); ]);
res.json({ res.json(response);
InventoryChanges: inventoryChanges
});
}; };
interface IGiftingRequest { interface IGiftingRequest {

View File

@ -1,11 +1,13 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { sendWsBroadcastTo } from "@/src/services/wsService";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { addMiscItems, getInventory } from "@/src/services/inventoryService"; import { addMiscItems, getInventory } from "@/src/services/inventoryService";
import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
import { ArtifactPolarity, EquipmentFeatures, IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { ArtifactPolarity } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { ExportRecipes } from "warframe-public-export-plus"; import { ExportRecipes } from "warframe-public-export-plus";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { EquipmentFeatures, IEquipmentClient } from "@/src/types/equipmentTypes";
interface IGildWeaponRequest { interface IGildWeaponRequest {
ItemName: string; ItemName: string;
@ -72,4 +74,5 @@ export const gildWeaponController: RequestHandler = async (req, res) => {
InventoryChanges: inventoryChanges, InventoryChanges: inventoryChanges,
AffiliationMods: affiliationMods AffiliationMods: affiliationMods
}); });
sendWsBroadcastTo(accountId, { update_inventory: true });
}; };

View File

@ -1,7 +1,8 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { addLoreFragmentScans, addShipDecorations, getInventory } from "@/src/services/inventoryService"; import { addLoreFragmentScans, addShipDecorations, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { ILoreFragmentScan, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes"; import { ITypeCount } from "@/src/types/commonTypes";
import { ILoreFragmentScan } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
export const giveShipDecoAndLoreFragmentController: RequestHandler = async (req, res) => { export const giveShipDecoAndLoreFragmentController: RequestHandler = async (req, res) => {

View File

@ -5,13 +5,14 @@ import {
getGuildVault, getGuildVault,
hasAccessToDojo, hasAccessToDojo,
hasGuildPermission, hasGuildPermission,
processCompletedGuildTechProject,
processFundedGuildTechProject, processFundedGuildTechProject,
processGuildTechProjectContributionsUpdate, processGuildTechProjectContributionsUpdate,
removePigmentsFromGuildMembers, removePigmentsFromGuildMembers,
scaleRequiredCount, scaleRequiredCount,
setGuildTechLogState setGuildTechLogState
} from "@/src/services/guildService"; } from "@/src/services/guildService";
import { ExportDojoRecipes } from "warframe-public-export-plus"; import { ExportDojoRecipes, ExportRailjackWeapons } from "warframe-public-export-plus";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { import {
addCrewShipWeaponSkin, addCrewShipWeaponSkin,
@ -51,8 +52,12 @@ export const guildTechController: RequestHandler = async (req, res) => {
}; };
if (project.CompletionDate) { if (project.CompletionDate) {
techProject.CompletionDate = toMongoDate(project.CompletionDate); techProject.CompletionDate = toMongoDate(project.CompletionDate);
if (Date.now() >= project.CompletionDate.getTime()) { if (
needSave ||= setGuildTechLogState(guild, project.ItemType, 4, project.CompletionDate); Date.now() >= project.CompletionDate.getTime() &&
setGuildTechLogState(guild, project.ItemType, 4, project.CompletionDate)
) {
processCompletedGuildTechProject(guild, project.ItemType);
needSave = true;
} }
} }
techProjects.push(techProject); techProjects.push(techProject);
@ -442,6 +447,7 @@ const finishComponentRepair = (
...(category == "CrewShipWeaponSkins" ...(category == "CrewShipWeaponSkins"
? addCrewShipWeaponSkin(inventory, salvageItem.ItemType, salvageItem.UpgradeFingerprint) ? addCrewShipWeaponSkin(inventory, salvageItem.ItemType, salvageItem.UpgradeFingerprint)
: addEquipment(inventory, category, salvageItem.ItemType, { : addEquipment(inventory, category, salvageItem.ItemType, {
UpgradeType: ExportRailjackWeapons[salvageItem.ItemType].defaultUpgrades?.[0].ItemType,
UpgradeFingerprint: salvageItem.UpgradeFingerprint UpgradeFingerprint: salvageItem.UpgradeFingerprint
})), })),
...occupySlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS, false) ...occupySlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS, false)

View File

@ -13,7 +13,8 @@ import {
addItems, addItems,
combineInventoryChanges, combineInventoryChanges,
getEffectiveAvatarImageType, getEffectiveAvatarImageType,
getInventory getInventory,
updateCurrency
} from "@/src/services/inventoryService"; } from "@/src/services/inventoryService";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { ExportFlavour } from "warframe-public-export-plus"; import { ExportFlavour } from "warframe-public-export-plus";
@ -100,6 +101,9 @@ export const inboxController: RequestHandler = async (req, res) => {
} }
} }
} }
if (message.RegularCredits) {
updateCurrency(inventory, -message.RegularCredits, false, inventoryChanges);
}
await inventory.save(); await inventory.save();
res.json({ InventoryChanges: inventoryChanges }); res.json({ InventoryChanges: inventoryChanges });
} else if (latestClientMessageId) { } else if (latestClientMessageId) {

View File

@ -30,8 +30,9 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
const request = getJSONfromString<IShardInstallRequest>(String(req.body)); const request = getJSONfromString<IShardInstallRequest>(String(req.body));
const inventory = await getInventory(account._id.toString()); const inventory = await getInventory(account._id.toString());
const suit = inventory.Suits.id(request.SuitId.$oid)!; const suit = inventory.Suits.id(request.SuitId.$oid)!;
if (!suit.ArchonCrystalUpgrades || suit.ArchonCrystalUpgrades.length != 5) { suit.ArchonCrystalUpgrades ??= [];
suit.ArchonCrystalUpgrades = [{}, {}, {}, {}, {}]; while (suit.ArchonCrystalUpgrades.length < request.Slot) {
suit.ArchonCrystalUpgrades.push({});
} }
suit.ArchonCrystalUpgrades[request.Slot] = { suit.ArchonCrystalUpgrades[request.Slot] = {
UpgradeType: request.UpgradeType, UpgradeType: request.UpgradeType,
@ -92,7 +93,8 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
} }
// remove from suit // remove from suit
suit.ArchonCrystalUpgrades![request.Slot] = {}; suit.ArchonCrystalUpgrades![request.Slot].UpgradeType = undefined;
suit.ArchonCrystalUpgrades![request.Slot].Color = undefined;
await inventory.save(); await inventory.save();

View File

@ -5,30 +5,41 @@ import { config } from "@/src/services/configService";
import allDialogue from "@/static/fixed_responses/allDialogue.json"; import allDialogue from "@/static/fixed_responses/allDialogue.json";
import { ILoadoutDatabase } from "@/src/types/saveLoadoutTypes"; import { ILoadoutDatabase } from "@/src/types/saveLoadoutTypes";
import { IInventoryClient, IShipInventory, equipmentKeys } from "@/src/types/inventoryTypes/inventoryTypes"; import { IInventoryClient, IShipInventory, equipmentKeys } from "@/src/types/inventoryTypes/inventoryTypes";
import { IPolarity, ArtifactPolarity, EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { IPolarity, ArtifactPolarity } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { import {
eFaction,
ExportCustoms, ExportCustoms,
ExportFlavour, ExportFlavour,
ExportRegions,
ExportResources, ExportResources,
ExportVirtuals ExportVirtuals,
ICountedItem
} from "warframe-public-export-plus"; } from "warframe-public-export-plus";
import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "@/src/services/infestedFoundryService"; import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "@/src/services/infestedFoundryService";
import { import {
addEmailItem,
addItem,
addMiscItems, addMiscItems,
allDailyAffiliationKeys, allDailyAffiliationKeys,
checkCalendarAutoAdvance,
cleanupInventory, cleanupInventory,
createLibraryDailyTask, createLibraryDailyTask,
generateRewardSeed getCalendarProgress
} from "@/src/services/inventoryService"; } from "@/src/services/inventoryService";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { catBreadHash } from "@/src/helpers/stringHelpers"; import { addString, catBreadHash } from "@/src/helpers/stringHelpers";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { getNemesisManifest } from "@/src/helpers/nemesisHelpers"; import { getNemesisManifest } from "@/src/helpers/nemesisHelpers";
import { getPersonalRooms } from "@/src/services/personalRoomsService"; import { getPersonalRooms } from "@/src/services/personalRoomsService";
import { IPersonalRoomsClient } from "@/src/types/personalRoomsTypes"; import { IPersonalRoomsClient } from "@/src/types/personalRoomsTypes";
import { Ship } from "@/src/models/shipModel"; import { Ship } from "@/src/models/shipModel";
import { toLegacyOid, version_compare } from "@/src/helpers/inventoryHelpers"; import { toLegacyOid, toOid, version_compare } from "@/src/helpers/inventoryHelpers";
import { Inbox } from "@/src/models/inboxModel";
import { unixTimesInMs } from "@/src/constants/timeConstants";
import { DailyDeal } from "@/src/models/worldStateModel";
import { EquipmentFeatures } from "@/src/types/equipmentTypes";
import { generateRewardSeed } from "@/src/services/rngService";
import { getInvasionByOid, getWorldState } from "@/src/services/worldStateService";
import { createMessage } from "@/src/services/inboxService";
export const inventoryController: RequestHandler = async (request, response) => { export const inventoryController: RequestHandler = async (request, response) => {
const account = await getAccountForRequest(request); const account = await getAccountForRequest(request);
@ -42,6 +53,8 @@ export const inventoryController: RequestHandler = async (request, response) =>
// Handle daily reset // Handle daily reset
if (!inventory.NextRefill || Date.now() >= inventory.NextRefill.getTime()) { if (!inventory.NextRefill || Date.now() >= inventory.NextRefill.getTime()) {
const today = Math.trunc(Date.now() / 86400000);
for (const key of allDailyAffiliationKeys) { for (const key of allDailyAffiliationKeys) {
inventory[key] = 16000 + inventory.PlayerLevel * 500; inventory[key] = 16000 + inventory.PlayerLevel * 500;
} }
@ -52,12 +65,12 @@ export const inventoryController: RequestHandler = async (request, response) =>
inventory.LibraryAvailableDailyTaskInfo = createLibraryDailyTask(); inventory.LibraryAvailableDailyTaskInfo = createLibraryDailyTask();
if (inventory.NextRefill) { if (inventory.NextRefill) {
const lastLoginDay = Math.trunc(inventory.NextRefill.getTime() / 86400000) - 1;
const daysPassed = today - lastLoginDay;
if (config.noArgonCrystalDecay) { if (config.noArgonCrystalDecay) {
inventory.FoundToday = undefined; inventory.FoundToday = undefined;
} else { } else {
const lastLoginDay = Math.trunc(inventory.NextRefill.getTime() / 86400000) - 1;
const today = Math.trunc(Date.now() / 86400000);
const daysPassed = today - lastLoginDay;
for (let i = 0; i != daysPassed; ++i) { for (let i = 0; i != daysPassed; ++i) {
const numArgonCrystals = const numArgonCrystals =
inventory.MiscItems.find(x => x.ItemType == "/Lotus/Types/Items/MiscItems/ArgonCrystal") inventory.MiscItems.find(x => x.ItemType == "/Lotus/Types/Items/MiscItems/ArgonCrystal")
@ -89,11 +102,87 @@ export const inventoryController: RequestHandler = async (request, response) =>
inventory.FoundToday = undefined; inventory.FoundToday = undefined;
} }
} }
if (inventory.UsedDailyDeals.length != 0) {
if (daysPassed == 1) {
const todayAt0Utc = today * 86400000;
const darvoIndex = Math.trunc((todayAt0Utc - 25200000) / (26 * unixTimesInMs.hour));
const darvoStart = darvoIndex * (26 * unixTimesInMs.hour) + 25200000;
const darvoOid =
((darvoStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "adc51a72f7324d95";
const deal = await DailyDeal.findById(darvoOid);
if (deal) {
inventory.UsedDailyDeals = inventory.UsedDailyDeals.filter(x => x == deal.StoreItem); // keep only the deal that came into this new day with us
} else {
inventory.UsedDailyDeals = [];
}
} else {
inventory.UsedDailyDeals = [];
}
}
}
// TODO: Setup CalendarProgress as part of 1999 mission completion?
const previousYearIteration = inventory.CalendarProgress?.Iteration;
// We need to do the following to ensure the in-game calendar does not break:
getCalendarProgress(inventory); // Keep the CalendarProgress up-to-date (at least for the current year iteration) (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/2364)
checkCalendarAutoAdvance(inventory, getWorldState().KnownCalendarSeasons[0]); // Skip birthday events for characters if we do not have them unlocked yet (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/2424)
// also handle sending of kiss cinematic at year rollover
if (
inventory.CalendarProgress!.Iteration != previousYearIteration &&
inventory.DialogueHistory &&
inventory.DialogueHistory.Dialogues
) {
let kalymos = false;
for (const { dialogueName, kissEmail } of [
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/ArthurDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/ArthurKissEmailItem"
},
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/EleanorDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/EleanorKissEmailItem"
},
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/LettieDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/LettieKissEmailItem"
},
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/JabirDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/AmirKissEmailItem"
},
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/AoiDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/AoiKissEmailItem"
},
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/QuincyDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/QuincyKissEmailItem"
}
]) {
const dialogue = inventory.DialogueHistory.Dialogues.find(x => x.DialogueName == dialogueName);
if (dialogue) {
if (dialogue.Rank == 7) {
await addEmailItem(inventory, kissEmail);
kalymos = false;
break;
}
if (dialogue.Rank == 6) {
kalymos = true;
}
}
}
if (kalymos) {
await addEmailItem(inventory, "/Lotus/Types/Items/EmailItems/KalymosKissEmailItem");
}
} }
cleanupInventory(inventory); cleanupInventory(inventory);
inventory.NextRefill = new Date((Math.trunc(Date.now() / 86400000) + 1) * 86400000); inventory.NextRefill = new Date((today + 1) * 86400000); // tomorrow at 0 UTC
//await inventory.save(); //await inventory.save();
} }
@ -106,6 +195,63 @@ export const inventoryController: RequestHandler = async (request, response) =>
//await inventory.save(); //await inventory.save();
} }
for (let i = 0; i != inventory.QualifyingInvasions.length; ) {
const qi = inventory.QualifyingInvasions[i];
const invasion = getInvasionByOid(qi.invasionId.toString());
if (!invasion) {
logger.debug(`removing QualifyingInvasions entry for unknown invasion: ${qi.invasionId.toString()}`);
inventory.QualifyingInvasions.splice(i, 1);
continue;
}
if (invasion.Completed) {
let factionSidedWith: string | undefined;
let battlePay: ICountedItem[] | undefined;
if (qi.AttackerScore >= 3) {
factionSidedWith = invasion.Faction;
battlePay = invasion.AttackerReward.countedItems;
logger.debug(`invasion pay from ${factionSidedWith}`, { battlePay });
} else if (qi.DefenderScore >= 3) {
factionSidedWith = invasion.DefenderFaction;
battlePay = invasion.DefenderReward.countedItems;
logger.debug(`invasion pay from ${factionSidedWith}`, { battlePay });
}
if (factionSidedWith) {
if (battlePay) {
// Decoupling rewards from the inbox message because it may delete itself without being read
for (const item of battlePay) {
await addItem(inventory, item.ItemType, item.ItemCount);
}
await createMessage(account._id, [
{
sndr: eFaction.find(x => x.tag == factionSidedWith)?.name ?? factionSidedWith, // TOVERIFY
msg: `/Lotus/Language/G1Quests/${factionSidedWith}_InvasionThankyouMessageBody`,
sub: `/Lotus/Language/G1Quests/${factionSidedWith}_InvasionThankyouMessageSubject`,
countedAtt: battlePay,
attVisualOnly: true,
icon:
factionSidedWith == "FC_GRINEER"
? "/Lotus/Interface/Icons/Npcs/EliteRifleLancerAvatar.png" // Source: https://www.reddit.com/r/Warframe/comments/1aj4usx/battle_pay_worth_10_plat/, https://www.youtube.com/watch?v=XhNZ6ai6BOY
: "/Lotus/Interface/Icons/Npcs/CrewmanNormal.png", // My best source for this is https://www.youtube.com/watch?v=rxrCCFm73XE around 1:37
// TOVERIFY: highPriority?
endDate: new Date(Date.now() + 86400_000) // TOVERIFY: This type of inbox message seems to automatically delete itself. We'll just delete it after 24 hours, but it's not clear if this is correct.
}
]);
}
if (invasion.Faction != "FC_INFESTATION") {
// Sided with grineer -> opposed corpus -> send zanuka (harvester)
// Sided with corpus -> opposed grineer -> send g3 (death squad)
inventory[factionSidedWith != "FC_GRINEER" ? "DeathSquadable" : "Harvestable"] = true;
// TOVERIFY: Should this happen earlier?
// TOVERIFY: Should this send an (ephemeral) email?
}
}
logger.debug(`removing QualifyingInvasions entry for completed invasion: ${qi.invasionId.toString()}`);
inventory.QualifyingInvasions.splice(i, 1);
continue;
}
++i;
}
if (inventory.LastInventorySync) { if (inventory.LastInventorySync) {
const lastSyncDuviriMood = Math.trunc(inventory.LastInventorySync.getTimestamp().getTime() / 7200000); const lastSyncDuviriMood = Math.trunc(inventory.LastInventorySync.getTimestamp().getTime() / 7200000);
const currentDuviriMood = Math.trunc(Date.now() / 7200000); const currentDuviriMood = Math.trunc(Date.now() / 7200000);
@ -134,13 +280,21 @@ export const getInventoryResponse = async (
xpBasedLevelCapDisabled: boolean, xpBasedLevelCapDisabled: boolean,
buildLabel: string | undefined buildLabel: string | undefined
): Promise<IInventoryClient> => { ): Promise<IInventoryClient> => {
const [inventoryWithLoadOutPresets, ships] = await Promise.all([ const [inventoryWithLoadOutPresets, ships, latestMessage] = await Promise.all([
inventory.populate<{ LoadOutPresets: ILoadoutDatabase }>("LoadOutPresets"), inventory.populate<{ LoadOutPresets: ILoadoutDatabase }>("LoadOutPresets"),
Ship.find({ ShipOwnerId: inventory.accountOwnerId }) Ship.find({ ShipOwnerId: inventory.accountOwnerId }),
Inbox.findOne({ ownerId: inventory.accountOwnerId }, "_id").sort({ date: -1 })
]); ]);
const inventoryResponse = inventoryWithLoadOutPresets.toJSON<IInventoryClient>(); const inventoryResponse = inventoryWithLoadOutPresets.toJSON<IInventoryClient>();
inventoryResponse.Ships = ships.map(x => x.toJSON<IShipInventory>()); inventoryResponse.Ships = ships.map(x => x.toJSON<IShipInventory>());
// In case mission inventory update added an inbox message, we need to send the Mailbox part so the client knows to refresh it.
if (latestMessage) {
inventoryResponse.Mailbox = {
LastInboxId: toOid(latestMessage._id)
};
}
if (config.infiniteCredits) { if (config.infiniteCredits) {
inventoryResponse.RegularCredits = 999999999; inventoryResponse.RegularCredits = 999999999;
} }
@ -167,18 +321,6 @@ export const getInventoryResponse = async (
} }
} }
if (config.unlockAllMissions) {
inventoryResponse.Missions = [];
for (const tag of Object.keys(ExportRegions)) {
inventoryResponse.Missions.push({
Completes: 1,
Tier: 1,
Tag: tag
});
}
addString(inventoryResponse.NodeIntrosCompleted, "TeshinHardModeUnlocked");
}
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)) {
@ -193,6 +335,17 @@ export const getInventoryResponse = async (
for (const uniqueName in ExportFlavour) { for (const uniqueName in ExportFlavour) {
inventoryResponse.FlavourItems.push({ ItemType: uniqueName }); inventoryResponse.FlavourItems.push({ ItemType: uniqueName });
} }
} else if (config.worldState?.baroTennoConRelay) {
[
"/Lotus/Types/Items/Events/TennoConRelay2022EarlyAccess",
"/Lotus/Types/Items/Events/TennoConRelay2023EarlyAccess",
"/Lotus/Types/Items/Events/TennoConRelay2024EarlyAccess",
"/Lotus/Types/Items/Events/TennoConRelay2025EarlyAccess"
].forEach(uniqueName => {
if (!inventoryResponse.FlavourItems.some(x => x.ItemType == uniqueName)) {
inventoryResponse.FlavourItems.push({ ItemType: uniqueName });
}
});
} }
if (config.unlockAllSkins) { if (config.unlockAllSkins) {
@ -300,9 +453,6 @@ export const getInventoryResponse = async (
applyCheatsToInfestedFoundry(inventoryResponse.InfestedFoundry); applyCheatsToInfestedFoundry(inventoryResponse.InfestedFoundry);
} }
// Omitting this field so opening the navigation resyncs the inventory which is more desirable for typical usage.
inventoryResponse.LastInventorySync = undefined;
// Set 2FA enabled so trading post can be used // Set 2FA enabled so trading post can be used
inventoryResponse.HWIDProtectEnabled = true; inventoryResponse.HWIDProtectEnabled = true;
@ -310,7 +460,7 @@ export const getInventoryResponse = async (
// Fix nemesis for older versions // Fix nemesis for older versions
if ( if (
inventoryResponse.Nemesis && inventoryResponse.Nemesis &&
version_compare(getNemesisManifest(inventoryResponse.Nemesis.manifest).minBuild, buildLabel) < 0 version_compare(buildLabel, getNemesisManifest(inventoryResponse.Nemesis.manifest).minBuild) < 0
) { ) {
inventoryResponse.Nemesis = undefined; inventoryResponse.Nemesis = undefined;
} }
@ -339,14 +489,41 @@ export const getInventoryResponse = async (
} }
} }
if (config.unlockAllProfitTakerStages) {
inventoryResponse.CompletedJobChains ??= [];
const EudicoHeists = inventoryResponse.CompletedJobChains.find(x => x.LocationTag == "EudicoHeists");
if (EudicoHeists) {
EudicoHeists.Jobs = allEudicoHeistJobs;
} else {
inventoryResponse.CompletedJobChains.push({
LocationTag: "EudicoHeists",
Jobs: allEudicoHeistJobs
});
}
}
if (config.unlockAllSimarisResearchEntries) {
inventoryResponse.LibraryPersonalTarget = undefined;
inventoryResponse.LibraryPersonalProgress = [
"/Lotus/Types/Game/Library/Targets/Research1Target",
"/Lotus/Types/Game/Library/Targets/Research2Target",
"/Lotus/Types/Game/Library/Targets/Research3Target",
"/Lotus/Types/Game/Library/Targets/Research4Target",
"/Lotus/Types/Game/Library/Targets/Research5Target",
"/Lotus/Types/Game/Library/Targets/Research6Target",
"/Lotus/Types/Game/Library/Targets/Research7Target"
].map(type => ({ TargetType: type, Scans: 10, Completed: true }));
}
return inventoryResponse; return inventoryResponse;
}; };
const addString = (arr: string[], str: string): void => { const allEudicoHeistJobs = [
if (arr.indexOf(str) == -1) { "/Lotus/Types/Gameplay/Venus/Jobs/Heists/HeistProfitTakerBountyOne",
arr.push(str); "/Lotus/Types/Gameplay/Venus/Jobs/Heists/HeistProfitTakerBountyTwo",
} "/Lotus/Types/Gameplay/Venus/Jobs/Heists/HeistProfitTakerBountyThree",
}; "/Lotus/Types/Gameplay/Venus/Jobs/Heists/HeistProfitTakerBountyFour"
];
const getExpRequiredForMr = (rank: number): number => { const getExpRequiredForMr = (rank: number): number => {
if (rank <= 30) { if (rank <= 30) {

View File

@ -1,9 +1,8 @@
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory, updateCurrency } from "@/src/services/inventoryService"; import { getInventory, updateCurrency, updateSlots } from "@/src/services/inventoryService";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { updateSlots } from "@/src/services/inventoryService";
import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger"; import { exhaustive } from "@/src/utils/ts-utils";
/* /*
loadout slots are additionally purchased slots only loadout slots are additionally purchased slots only
@ -23,13 +22,44 @@ 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;
if (body.Bin != InventorySlot.SUITS && body.Bin != InventorySlot.PVE_LOADOUTS) { let price;
logger.warn(`unexpected slot purchase of type ${body.Bin}, account may be overcharged`); let amount;
switch (body.Bin) {
case InventorySlot.SUITS:
case InventorySlot.MECHSUITS:
case InventorySlot.PVE_LOADOUTS:
case InventorySlot.CREWMEMBERS:
price = 20;
amount = 1;
break;
case InventorySlot.SPACESUITS:
price = 12;
amount = 1;
break;
case InventorySlot.WEAPONS:
case InventorySlot.SPACEWEAPONS:
case InventorySlot.SENTINELS:
case InventorySlot.RJ_COMPONENT_AND_ARMAMENTS:
case InventorySlot.AMPS:
price = 12;
amount = 2;
break;
case InventorySlot.RIVENS:
price = 60;
amount = 3;
break;
default:
exhaustive(body.Bin);
throw new Error(`unexpected slot purchase of type ${body.Bin as string}`);
} }
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const currencyChanges = updateCurrency(inventory, 20, true); const currencyChanges = updateCurrency(inventory, price, true);
updateSlots(inventory, body.Bin, 1, 1); updateSlots(inventory, body.Bin, amount, amount);
await inventory.save(); await inventory.save();
res.json({ InventoryChanges: currencyChanges }); res.json({ InventoryChanges: currencyChanges });

View File

@ -4,16 +4,16 @@ import { config } from "@/src/services/configService";
import { buildConfig } from "@/src/services/buildConfigService"; import { buildConfig } from "@/src/services/buildConfigService";
import { Account } from "@/src/models/loginModel"; import { Account } from "@/src/models/loginModel";
import { createAccount, isCorrectPassword, isNameTaken } from "@/src/services/loginService"; import { createAccount, createNonce, getUsernameFromEmail, isCorrectPassword } from "@/src/services/loginService";
import { IDatabaseAccountJson, ILoginRequest, ILoginResponse } from "@/src/types/loginTypes"; import { IDatabaseAccountJson, ILoginRequest, ILoginResponse } from "@/src/types/loginTypes";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { version_compare } from "@/src/helpers/inventoryHelpers"; import { version_compare } from "@/src/helpers/inventoryHelpers";
import { sendWsBroadcastTo } from "@/src/services/wsService";
export const loginController: RequestHandler = async (request, response) => { export const loginController: RequestHandler = async (request, response) => {
const loginRequest = JSON.parse(String(request.body)) as ILoginRequest; // parse octet stream of json data to json object const loginRequest = JSON.parse(String(request.body)) as ILoginRequest; // parse octet stream of json data to json object
const account = await Account.findOne({ email: loginRequest.email }); const account = await Account.findOne({ email: loginRequest.email });
const nonce = Math.round(Math.random() * Number.MAX_SAFE_INTEGER);
const buildLabel: string = const buildLabel: string =
typeof request.query.buildLabel == "string" typeof request.query.buildLabel == "string"
@ -42,26 +42,14 @@ export const loginController: RequestHandler = async (request, response) => {
loginRequest.ClientType == "webui-register") loginRequest.ClientType == "webui-register")
) { ) {
try { try {
const nameFromEmail = loginRequest.email.substring(0, loginRequest.email.indexOf("@")); const name = await getUsernameFromEmail(loginRequest.email);
let name = nameFromEmail || loginRequest.email.substring(1) || "SpaceNinja";
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: name, DisplayName: name,
CountryCode: loginRequest.lang?.toUpperCase() ?? "EN", CountryCode: loginRequest.lang?.toUpperCase() ?? "EN",
ClientType: loginRequest.ClientType == "webui-register" ? "webui" : loginRequest.ClientType, ClientType: loginRequest.ClientType,
CrossPlatformAllowed: true, Nonce: createNonce(),
ForceLogoutVersion: 0,
ConsentNeeded: false,
TrackedSettings: [],
Nonce: nonce,
BuildLabel: buildLabel, BuildLabel: buildLabel,
LastLogin: new Date() LastLogin: new Date()
}); });
@ -80,38 +68,29 @@ export const loginController: RequestHandler = async (request, response) => {
return; return;
} }
if (loginRequest.ClientType == "webui-register") {
response.status(400).json({ error: "account already exists" });
return;
}
if (!isCorrectPassword(loginRequest.password, account.password)) { if (!isCorrectPassword(loginRequest.password, account.password)) {
response.status(400).json({ error: "incorrect login data" }); response.status(400).json({ error: "incorrect login data" });
return; return;
} }
if (loginRequest.ClientType == "webui") { if (account.Nonce && account.ClientType != "webui" && !account.Dropped && !loginRequest.kick) {
if (!account.Nonce) { // U17 seems to handle "nonce still set" like a login failure.
account.ClientType = "webui"; if (version_compare(buildLabel, "2015.12.05.18.07") >= 0) {
account.Nonce = nonce; response.status(400).send({ error: "nonce still set" });
return;
} }
} else {
if (account.Nonce && account.ClientType != "webui" && !account.Dropped && !loginRequest.kick) {
// U17 seems to handle "nonce still set" like a login failure.
if (version_compare(buildLabel, "2015.12.05.18.07") >= 0) {
response.status(400).send({ error: "nonce still set" });
return;
}
}
account.ClientType = loginRequest.ClientType;
account.Nonce = nonce;
account.CountryCode = loginRequest.lang?.toUpperCase() ?? "EN";
account.BuildLabel = buildLabel;
account.LastLogin = new Date();
} }
account.ClientType = loginRequest.ClientType;
account.Nonce = createNonce();
account.CountryCode = loginRequest.lang?.toUpperCase() ?? "EN";
account.BuildLabel = buildLabel;
account.LastLogin = new Date();
await account.save(); await account.save();
// Tell WebUI its nonce has been invalidated
sendWsBroadcastTo(account._id.toString(), { logged_out: true });
response.json(createLoginResponse(myAddress, myUrlBase, account.toJSON(), buildLabel)); response.json(createLoginResponse(myAddress, myUrlBase, account.toJSON(), buildLabel));
}; };
@ -151,7 +130,7 @@ const createLoginResponse = (
resp.Groups = []; resp.Groups = [];
} }
if (version_compare(buildLabel, "2021.04.13.19.58") >= 0) { if (version_compare(buildLabel, "2021.04.13.19.58") >= 0) {
resp.DTLS = 99; resp.DTLS = 0; // bit 0 enables DTLS. if enabled, additional bits can be set, e.g. bit 2 to enable logging. on live, the value is 99.
} }
if (version_compare(buildLabel, "2022.04.29.12.53") >= 0) { if (version_compare(buildLabel, "2022.04.29.12.53") >= 0) {
resp.ClientType = account.ClientType; resp.ClientType = account.ClientType;

View File

@ -8,6 +8,8 @@ import {
setAccountGotLoginRewardToday setAccountGotLoginRewardToday
} from "@/src/services/loginRewardService"; } from "@/src/services/loginRewardService";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { config } from "@/src/services/configService";
import { sendWsBroadcastTo } from "@/src/services/wsService";
export const loginRewardsController: RequestHandler = async (req, res) => { export const loginRewardsController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req); const account = await getAccountForRequest(req);
@ -15,7 +17,7 @@ export const loginRewardsController: RequestHandler = async (req, res) => {
const isMilestoneDay = account.LoginDays == 5 || account.LoginDays % 50 == 0; const isMilestoneDay = account.LoginDays == 5 || account.LoginDays % 50 == 0;
const nextMilestoneDay = account.LoginDays < 5 ? 5 : (Math.trunc(account.LoginDays / 50) + 1) * 50; const nextMilestoneDay = account.LoginDays < 5 ? 5 : (Math.trunc(account.LoginDays / 50) + 1) * 50;
if (today == account.LastLoginRewardDate) { if (today == account.LastLoginRewardDate || config.disableDailyTribute) {
res.json({ res.json({
DailyTributeInfo: { DailyTributeInfo: {
IsMilestoneDay: isMilestoneDay, IsMilestoneDay: isMilestoneDay,
@ -46,10 +48,10 @@ export const loginRewardsController: RequestHandler = async (req, res) => {
response.DailyTributeInfo.HasChosenReward = true; response.DailyTributeInfo.HasChosenReward = true;
response.DailyTributeInfo.ChosenReward = randomRewards[0]; response.DailyTributeInfo.ChosenReward = randomRewards[0];
response.DailyTributeInfo.NewInventory = await claimLoginReward(inventory, randomRewards[0]); response.DailyTributeInfo.NewInventory = await claimLoginReward(inventory, randomRewards[0]);
await inventory.save();
setAccountGotLoginRewardToday(account); setAccountGotLoginRewardToday(account);
await account.save(); await Promise.all([inventory.save(), account.save()]);
sendWsBroadcastTo(account._id.toString(), { update_inventory: true });
} }
res.json(response); res.json(response);
}; };

View File

@ -6,6 +6,7 @@ import {
} from "@/src/services/loginRewardService"; } from "@/src/services/loginRewardService";
import { getAccountForRequest } from "@/src/services/loginService"; import { getAccountForRequest } from "@/src/services/loginService";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { sendWsBroadcastTo } from "@/src/services/wsService";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
@ -34,11 +35,10 @@ export const loginRewardsSelectionController: RequestHandler = async (req, res)
chosenReward = randomRewards.find(x => x.StoreItemType == body.ChosenReward)!; chosenReward = randomRewards.find(x => x.StoreItemType == body.ChosenReward)!;
inventoryChanges = await claimLoginReward(inventory, chosenReward); inventoryChanges = await claimLoginReward(inventory, chosenReward);
} }
await inventory.save();
setAccountGotLoginRewardToday(account); setAccountGotLoginRewardToday(account);
await account.save(); await Promise.all([inventory.save(), account.save()]);
sendWsBroadcastTo(account._id.toString(), { update_inventory: true });
res.json({ res.json({
DailyTributeInfo: { DailyTributeInfo: {
NewInventory: inventoryChanges, NewInventory: inventoryChanges,

View File

@ -1,5 +1,6 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { Account } from "@/src/models/loginModel"; import { Account } from "@/src/models/loginModel";
import { sendWsBroadcastTo } from "@/src/services/wsService";
export const logoutController: RequestHandler = async (req, res) => { export const logoutController: RequestHandler = async (req, res) => {
if (!req.query.accountId) { if (!req.query.accountId) {
@ -10,7 +11,7 @@ export const logoutController: RequestHandler = async (req, res) => {
throw new Error("Request is missing nonce parameter"); throw new Error("Request is missing nonce parameter");
} }
await Account.updateOne( const stat = await Account.updateOne(
{ {
_id: req.query.accountId, _id: req.query.accountId,
Nonce: nonce Nonce: nonce
@ -19,6 +20,10 @@ export const logoutController: RequestHandler = async (req, res) => {
Nonce: 0 Nonce: 0
} }
); );
if (stat.modifiedCount) {
// Tell WebUI its nonce has been invalidated
sendWsBroadcastTo(req.query.accountId as string, { logged_out: true });
}
res.writeHead(200, { res.writeHead(200, {
"Content-Type": "text/html", "Content-Type": "text/html",

View File

@ -3,10 +3,16 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getAccountForRequest } from "@/src/services/loginService"; import { getAccountForRequest } from "@/src/services/loginService";
import { IMissionInventoryUpdateRequest } from "@/src/types/requestTypes"; import { IMissionInventoryUpdateRequest } from "@/src/types/requestTypes";
import { addMissionInventoryUpdates, addMissionRewards } from "@/src/services/missionInventoryUpdateService"; import { addMissionInventoryUpdates, addMissionRewards } from "@/src/services/missionInventoryUpdateService";
import { generateRewardSeed, getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { getInventoryResponse } from "./inventoryController"; import { getInventoryResponse } from "@/src/controllers/api/inventoryController";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { IMissionInventoryUpdateResponse } from "@/src/types/missionTypes"; import {
IMissionInventoryUpdateResponse,
IMissionInventoryUpdateResponseBackToDryDock,
IMissionInventoryUpdateResponseRailjackInterstitial
} from "@/src/types/missionTypes";
import { sendWsBroadcastTo } from "@/src/services/wsService";
import { generateRewardSeed } from "@/src/services/rngService";
/* /*
**** INPUT **** **** INPUT ****
@ -76,6 +82,7 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
InventoryJson: JSON.stringify(inventoryResponse), InventoryJson: JSON.stringify(inventoryResponse),
MissionRewards: [] MissionRewards: []
}); });
sendWsBroadcastTo(account._id.toString(), { update_inventory: true });
return; return;
} }
@ -86,17 +93,15 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
AffiliationMods, AffiliationMods,
SyndicateXPItemReward, SyndicateXPItemReward,
ConquestCompletedMissionsCount ConquestCompletedMissionsCount
} = await addMissionRewards(inventory, missionReport, firstCompletion); } = await addMissionRewards(account, inventory, missionReport, firstCompletion);
if (missionReport.EndOfMatchUpload) { if (missionReport.EndOfMatchUpload) {
inventory.RewardSeed = generateRewardSeed(); inventory.RewardSeed = generateRewardSeed();
} }
await inventory.save(); await inventory.save();
const inventoryResponse = await getInventoryResponse(inventory, true, account.BuildLabel);
//TODO: figure out when to send inventory. it is needed for many cases. //TODO: figure out when to send inventory. it is needed for many cases.
res.json({ const deltas: IMissionInventoryUpdateResponseRailjackInterstitial = {
InventoryJson: JSON.stringify(inventoryResponse),
InventoryChanges: inventoryChanges, InventoryChanges: inventoryChanges,
MissionRewards, MissionRewards,
...credits, ...credits,
@ -105,7 +110,26 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
SyndicateXPItemReward, SyndicateXPItemReward,
AffiliationMods, AffiliationMods,
ConquestCompletedMissionsCount ConquestCompletedMissionsCount
} satisfies IMissionInventoryUpdateResponse); };
if (missionReport.RJ) {
logger.debug(`railjack interstitial request, sending only deltas`, deltas);
res.json(deltas);
} else if (missionReport.RewardInfo) {
logger.debug(`classic mission completion, sending everything`);
const inventoryResponse = await getInventoryResponse(inventory, true, account.BuildLabel);
res.json({
InventoryJson: JSON.stringify(inventoryResponse),
...deltas
} satisfies IMissionInventoryUpdateResponse);
} else {
logger.debug(`no reward info, assuming this wasn't a mission completion and we should just sync inventory`);
const inventoryResponse = await getInventoryResponse(inventory, true, account.BuildLabel);
res.json({
InventoryJson: JSON.stringify(inventoryResponse)
} satisfies IMissionInventoryUpdateResponseBackToDryDock);
}
sendWsBroadcastTo(account._id.toString(), { update_inventory: true });
}; };
/* /*

View File

@ -1,5 +1,6 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { sendWsBroadcastTo } from "@/src/services/wsService";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { import {
getInventory, getInventory,
@ -15,10 +16,9 @@ import {
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { getDefaultUpgrades } from "@/src/services/itemDataService"; import { getDefaultUpgrades } from "@/src/services/itemDataService";
import { modularWeaponTypes } from "@/src/helpers/modularWeaponHelper"; import { modularWeaponTypes } from "@/src/helpers/modularWeaponHelper";
import { IEquipmentDatabase } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { getRandomInt } from "@/src/services/rngService"; import { getRandomInt } from "@/src/services/rngService";
import { ExportSentinels, ExportWeapons, IDefaultUpgrade } from "warframe-public-export-plus"; import { ExportSentinels, ExportWeapons, IDefaultUpgrade } from "warframe-public-export-plus";
import { Status } from "@/src/types/inventoryTypes/inventoryTypes"; import { IEquipmentDatabase, Status } from "@/src/types/equipmentTypes";
interface IModularCraftRequest { interface IModularCraftRequest {
WeaponType: string; WeaponType: string;
@ -195,4 +195,5 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res)
MiscItems: miscItemChanges MiscItems: miscItemChanges
} }
}); });
sendWsBroadcastTo(accountId, { update_inventory: true });
}; };

View File

@ -3,7 +3,7 @@ import { ExportWeapons } from "warframe-public-export-plus";
import { IMongoDate } from "@/src/types/commonTypes"; import { IMongoDate } from "@/src/types/commonTypes";
import { toMongoDate } from "@/src/helpers/inventoryHelpers"; import { toMongoDate } from "@/src/helpers/inventoryHelpers";
import { SRng } from "@/src/services/rngService"; import { SRng } from "@/src/services/rngService";
import { ArtifactPolarity, EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { ArtifactPolarity } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { import {
addEquipment, addEquipment,
@ -15,8 +15,10 @@ import {
} from "@/src/services/inventoryService"; } from "@/src/services/inventoryService";
import { getDefaultUpgrades } from "@/src/services/itemDataService"; import { getDefaultUpgrades } from "@/src/services/itemDataService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { sendWsBroadcastTo } from "@/src/services/wsService";
import { modularWeaponTypes } from "@/src/helpers/modularWeaponHelper"; import { modularWeaponTypes } from "@/src/helpers/modularWeaponHelper";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { EquipmentFeatures } from "@/src/types/equipmentTypes";
export const modularWeaponSaleController: RequestHandler = async (req, res) => { export const modularWeaponSaleController: RequestHandler = async (req, res) => {
const partTypeToParts: Record<string, string[]> = {}; const partTypeToParts: Record<string, string[]> = {};
@ -67,6 +69,7 @@ export const modularWeaponSaleController: RequestHandler = async (req, res) => {
res.json({ res.json({
InventoryChanges: inventoryChanges InventoryChanges: inventoryChanges
}); });
sendWsBroadcastTo(accountId, { update_inventory: true });
} else { } else {
throw new Error(`unknown modularWeaponSale op: ${String(req.query.op)}`); throw new Error(`unknown modularWeaponSale op: ${String(req.query.op)}`);
} }

View File

@ -3,6 +3,7 @@ import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory, updateCurrency } from "@/src/services/inventoryService"; import { getInventory, updateCurrency } from "@/src/services/inventoryService";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
import { sendWsBroadcastTo } from "@/src/services/wsService";
interface INameWeaponRequest { interface INameWeaponRequest {
ItemName: string; ItemName: string;
@ -27,4 +28,5 @@ export const nameWeaponController: RequestHandler = async (req, res) => {
res.json({ res.json({
InventoryChanges: currencyChanges InventoryChanges: currencyChanges
}); });
sendWsBroadcastTo(accountId, { update_inventory: true });
}; };

View File

@ -1,21 +1,28 @@
import { version_compare } from "@/src/helpers/inventoryHelpers"; import { version_compare } from "@/src/helpers/inventoryHelpers";
import { import {
consumeModCharge, antivirusMods,
decodeNemesisGuess,
encodeNemesisGuess, encodeNemesisGuess,
getInfNodes, getInfNodes,
getKnifeUpgrade, getKnifeUpgrade,
getNemesisManifest, getNemesisManifest,
getNemesisPasscode, getNemesisPasscode,
getNemesisPasscodeModTypes, GUESS_CORRECT,
IKnifeResponse GUESS_INCORRECT,
GUESS_NEUTRAL,
GUESS_NONE,
GUESS_WILDCARD,
IKnifeResponse,
parseUpgrade
} from "@/src/helpers/nemesisHelpers"; } from "@/src/helpers/nemesisHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { Loadout } from "@/src/models/inventoryModels/loadoutModel"; import { Loadout } from "@/src/models/inventoryModels/loadoutModel";
import { freeUpSlot, getInventory } from "@/src/services/inventoryService"; import { addMods, freeUpSlot, getInventory } from "@/src/services/inventoryService";
import { getAccountForRequest } from "@/src/services/loginService"; import { getAccountForRequest } from "@/src/services/loginService";
import { SRng } from "@/src/services/rngService"; import { SRng } from "@/src/services/rngService";
import { IMongoDate, IOid } from "@/src/types/commonTypes"; import { IMongoDate, IOid } from "@/src/types/commonTypes";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { IEquipmentClient } from "@/src/types/equipmentTypes";
import { import {
IInnateDamageFingerprint, IInnateDamageFingerprint,
IInventoryClient, IInventoryClient,
@ -29,6 +36,7 @@ import {
} from "@/src/types/inventoryTypes/inventoryTypes"; } from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { Types } from "mongoose";
export const nemesisController: RequestHandler = async (req, res) => { export const nemesisController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req); const account = await getAccountForRequest(req);
@ -82,7 +90,7 @@ export const nemesisController: RequestHandler = async (req, res) => {
} }
} else { } else {
for (let i = 0; i != 3; ++i) { for (let i = 0; i != 3; ++i) {
if (body.guess[i] == passcode[i]) { if (body.guess[i] == passcode[i] || body.guess[i] == GUESS_WILDCARD) {
++guessResult; ++guessResult;
} }
} }
@ -97,18 +105,29 @@ export const nemesisController: RequestHandler = async (req, res) => {
if (inventory.Nemesis!.Faction == "FC_INFESTATION") { if (inventory.Nemesis!.Faction == "FC_INFESTATION") {
const guess: number[] = [body.guess & 0xf, (body.guess >> 4) & 0xf, (body.guess >> 8) & 0xf]; const guess: number[] = [body.guess & 0xf, (body.guess >> 4) & 0xf, (body.guess >> 8) & 0xf];
const passcode = getNemesisPasscode(inventory.Nemesis!)[0]; const passcode = getNemesisPasscode(inventory.Nemesis!)[0];
const result1 = passcode == guess[0] ? GUESS_CORRECT : GUESS_INCORRECT;
// Add to GuessHistory const result2 = passcode == guess[1] ? GUESS_CORRECT : GUESS_INCORRECT;
const result1 = passcode == guess[0] ? 0 : 1; const result3 = passcode == guess[2] ? GUESS_CORRECT : GUESS_INCORRECT;
const result2 = passcode == guess[1] ? 0 : 1;
const result3 = passcode == guess[2] ? 0 : 1;
inventory.Nemesis!.GuessHistory.push( inventory.Nemesis!.GuessHistory.push(
encodeNemesisGuess(guess[0], result1, guess[1], result2, guess[2], result3) encodeNemesisGuess([
{
symbol: guess[0],
result: result1
},
{
symbol: guess[1],
result: result2
},
{
symbol: guess[2],
result: result3
}
])
); );
// Increase antivirus if correct antivirus mod is installed // Increase antivirus if correct antivirus mod is installed
const response: IKnifeResponse = {}; const response: IKnifeResponse = {};
if (result1 == 0 || result2 == 0 || result3 == 0) { if (result1 == GUESS_CORRECT || result2 == GUESS_CORRECT || result3 == GUESS_CORRECT) {
let antivirusGain = 5; let antivirusGain = 5;
const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!; const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!;
const dataknifeLoadout = loadout.DATAKNIFE.id(inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE].$oid); const dataknifeLoadout = loadout.DATAKNIFE.id(inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE].$oid);
@ -117,50 +136,108 @@ export const nemesisController: RequestHandler = async (req, res) => {
for (const upgrade of body.knife!.AttachedUpgrades) { for (const upgrade of body.knife!.AttachedUpgrades) {
switch (upgrade.ItemType) { switch (upgrade.ItemType) {
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndSpeedOnUseMod": case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndSpeedOnUseMod":
antivirusGain += 10;
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
break;
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndWeaponDamageOnUseMod": case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndWeaponDamageOnUseMod":
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusSmallOnSingleUseMod":
antivirusGain += 10; antivirusGain += 10;
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades); consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
break; break;
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusLargeOnSingleUseMod": // Instant Secure case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusLargeOnSingleUseMod": // Instant Secure
antivirusGain += 15;
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
break;
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusOnUseMod": // Immuno Shield case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusOnUseMod": // Immuno Shield
antivirusGain += 15; antivirusGain += 15;
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades); consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
break; break;
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusSmallOnSingleUseMod":
antivirusGain += 10;
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
break;
} }
} }
inventory.Nemesis!.HenchmenKilled += antivirusGain; inventory.Nemesis!.HenchmenKilled += antivirusGain;
if (inventory.Nemesis!.HenchmenKilled >= 100) {
inventory.Nemesis!.HenchmenKilled = 100;
// Weaken nemesis now.
inventory.Nemesis!.InfNodes = [
{
Node: getNemesisManifest(inventory.Nemesis!.manifest).showdownNode,
Influence: 1
}
];
inventory.Nemesis!.Weakened = true;
const upgrade = getKnifeUpgrade(inventory, dataknifeUpgrades, antivirusMods[passcode]);
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
}
} }
if (inventory.Nemesis!.HenchmenKilled >= 100) { if (inventory.Nemesis!.HenchmenKilled < 100) {
inventory.Nemesis!.HenchmenKilled = 100; inventory.Nemesis!.InfNodes = getInfNodes(getNemesisManifest(inventory.Nemesis!.manifest), 0);
} }
inventory.Nemesis!.InfNodes = getInfNodes(getNemesisManifest(inventory.Nemesis!.manifest), 0);
await inventory.save(); await inventory.save();
res.json(response); res.json(response);
} else { } else {
const passcode = getNemesisPasscode(inventory.Nemesis!); // For first guess, create a new entry.
if (passcode[body.position] != body.guess) { if (body.position == 0) {
res.end(); inventory.Nemesis!.GuessHistory.push(
} else { encodeNemesisGuess([
inventory.Nemesis!.Rank += 1; {
inventory.Nemesis!.InfNodes = getInfNodes( symbol: GUESS_NONE,
getNemesisManifest(inventory.Nemesis!.manifest), result: GUESS_NEUTRAL
inventory.Nemesis!.Rank },
{
symbol: GUESS_NONE,
result: GUESS_NEUTRAL
},
{
symbol: GUESS_NONE,
result: GUESS_NEUTRAL
}
])
); );
await inventory.save();
res.json({ RankIncrease: 1 });
} }
// Evaluate guess
const correct =
body.guess == GUESS_WILDCARD || getNemesisPasscode(inventory.Nemesis!)[body.position] == body.guess;
// Update entry
const guess = decodeNemesisGuess(
inventory.Nemesis!.GuessHistory[inventory.Nemesis!.GuessHistory.length - 1]
);
guess[body.position].symbol = body.guess;
guess[body.position].result = correct ? GUESS_CORRECT : GUESS_INCORRECT;
inventory.Nemesis!.GuessHistory[inventory.Nemesis!.GuessHistory.length - 1] = encodeNemesisGuess(guess);
const response: INemesisRequiemResponse = {};
if (correct) {
if (body.position == 2) {
// That was all 3 guesses correct, nemesis is now weakened.
inventory.Nemesis!.InfNodes = [
{
Node: getNemesisManifest(inventory.Nemesis!.manifest).showdownNode,
Influence: 1
}
];
inventory.Nemesis!.Weakened = true;
// Subtract a charge from all requiem mods installed on parazon
const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!;
const dataknifeLoadout = loadout.DATAKNIFE.id(
inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE].$oid
);
const dataknifeConfigIndex = dataknifeLoadout?.s?.mod ?? 0;
const dataknifeUpgrades = inventory.DataKnives[0].Configs[dataknifeConfigIndex].Upgrades!;
for (let i = 3; i != 6; ++i) {
//logger.debug(`subtracting a charge from ${dataknifeUpgrades[i]}`);
const upgrade = parseUpgrade(inventory, dataknifeUpgrades[i]);
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
}
}
} else {
// Guess was incorrect, increase rank
response.RankIncrease = 1;
const manifest = getNemesisManifest(inventory.Nemesis!.manifest);
inventory.Nemesis!.Rank = Math.min(inventory.Nemesis!.Rank + 1, manifest.systemIndexes.length - 1);
inventory.Nemesis!.InfNodes = getInfNodes(manifest, inventory.Nemesis!.Rank);
}
await inventory.save();
res.json(response);
} }
} else if ((req.query.mode as string) == "rs") { } else if ((req.query.mode as string) == "rs") {
// report spawn; POST but no application data in body // report spawn; POST but no application data in body
@ -170,11 +247,14 @@ export const nemesisController: RequestHandler = async (req, res) => {
res.json({ LastEnc: inventory.Nemesis!.LastEnc }); res.json({ LastEnc: inventory.Nemesis!.LastEnc });
} else if ((req.query.mode as string) == "s") { } else if ((req.query.mode as string) == "s") {
const inventory = await getInventory(account._id.toString(), "Nemesis"); const inventory = await getInventory(account._id.toString(), "Nemesis");
if (inventory.Nemesis) {
logger.warn(`overwriting an existing nemesis as a new one is being requested`);
}
const body = getJSONfromString<INemesisStartRequest>(String(req.body)); const body = getJSONfromString<INemesisStartRequest>(String(req.body));
body.target.fp = BigInt(body.target.fp); body.target.fp = BigInt(body.target.fp);
const manifest = getNemesisManifest(body.target.manifest); const manifest = getNemesisManifest(body.target.manifest);
if (account.BuildLabel && version_compare(manifest.minBuild, account.BuildLabel) < 0) { if (account.BuildLabel && version_compare(account.BuildLabel, manifest.minBuild) < 0) {
logger.warn( logger.warn(
`client on version ${account.BuildLabel} provided nemesis manifest ${body.target.manifest} which was expected to require ${manifest.minBuild} or above. please file a bug report.` `client on version ${account.BuildLabel} provided nemesis manifest ${body.target.manifest} which was expected to require ${manifest.minBuild} or above. please file a bug report.`
); );
@ -185,13 +265,15 @@ export const nemesisController: RequestHandler = async (req, res) => {
const weapons: readonly string[] = manifest.weapons; const weapons: readonly string[] = manifest.weapons;
const initialWeaponIdx = new SRng(body.target.fp).randomInt(0, weapons.length - 1); const initialWeaponIdx = new SRng(body.target.fp).randomInt(0, weapons.length - 1);
weaponIdx = initialWeaponIdx; weaponIdx = initialWeaponIdx;
do { if (body.target.DisallowedWeapons) {
const weapon = weapons[weaponIdx]; do {
if (body.target.DisallowedWeapons.indexOf(weapon) == -1) { const weapon = weapons[weaponIdx];
break; if (body.target.DisallowedWeapons.indexOf(weapon) == -1) {
} break;
weaponIdx = (weaponIdx + 1) % weapons.length; }
} while (weaponIdx != initialWeaponIdx); weaponIdx = (weaponIdx + 1) % weapons.length;
} while (weaponIdx != initialWeaponIdx);
}
} }
inventory.Nemesis = { inventory.Nemesis = {
@ -212,10 +294,10 @@ export const nemesisController: RequestHandler = async (req, res) => {
GuessHistory: [], GuessHistory: [],
Hints: [], Hints: [],
HintProgress: 0, HintProgress: 0,
Weakened: body.target.Weakened, Weakened: false,
PrevOwners: 0, PrevOwners: 0,
HenchmenKilled: 0, HenchmenKilled: 0,
SecondInCommand: body.target.SecondInCommand, SecondInCommand: false,
MissionCount: 0, MissionCount: 0,
LastEnc: 0 LastEnc: 0
}; };
@ -225,36 +307,15 @@ export const nemesisController: RequestHandler = async (req, res) => {
target: inventory.toJSON().Nemesis target: inventory.toJSON().Nemesis
}); });
} else if ((req.query.mode as string) == "w") { } else if ((req.query.mode as string) == "w") {
const inventory = await getInventory( const inventory = await getInventory(account._id.toString(), "Nemesis");
account._id.toString(),
"Nemesis LoadOutPresets CurrentLoadOutIds DataKnives Upgrades RawUpgrades"
);
//const body = getJSONfromString<INemesisWeakenRequest>(String(req.body)); //const body = getJSONfromString<INemesisWeakenRequest>(String(req.body));
inventory.Nemesis!.InfNodes = [ // As of 38.6.0, this request is no longer sent, instead mode=r already weakens the nemesis if appropriate.
{ // We always weaken the nemesis in mode=r so simply giving the client back the nemesis.
Node: getNemesisManifest(inventory.Nemesis!.manifest).showdownNode,
Influence: 1
}
];
inventory.Nemesis!.Weakened = true;
const response: IKnifeResponse & { target: INemesisClient } = { const response: INemesisWeakenResponse = {
target: inventory.toJSON<IInventoryClient>().Nemesis! target: inventory.toJSON<IInventoryClient>().Nemesis!
}; };
// Consume charge of the correct requiem mod(s)
const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!;
const dataknifeLoadout = loadout.DATAKNIFE.id(inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE].$oid);
const dataknifeConfigIndex = dataknifeLoadout?.s?.mod ?? 0;
const dataknifeUpgrades = inventory.DataKnives[0].Configs[dataknifeConfigIndex].Upgrades!;
const modTypes = getNemesisPasscodeModTypes(inventory.Nemesis!);
for (const modType of modTypes) {
const upgrade = getKnifeUpgrade(inventory, dataknifeUpgrades, modType);
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
}
await inventory.save();
res.json(response); res.json(response);
} else { } else {
logger.debug(`data provided to ${req.path}: ${String(req.body)}`); logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
@ -276,7 +337,7 @@ interface INemesisStartRequest {
KillingSuit: string; KillingSuit: string;
killingDamageType: number; killingDamageType: number;
ShoulderHelmet: string; ShoulderHelmet: string;
DisallowedWeapons: string[]; DisallowedWeapons?: string[];
WeaponIdx: number; WeaponIdx: number;
AgentIdx: number; AgentIdx: number;
BirthNode: string; BirthNode: string;
@ -310,11 +371,19 @@ interface INemesisRequiemRequest {
knife?: IKnife; knife?: IKnife;
} }
interface INemesisRequiemResponse extends IKnifeResponse {
RankIncrease?: number;
}
// interface INemesisWeakenRequest { // interface INemesisWeakenRequest {
// target: INemesisClient; // target: INemesisClient;
// knife: IKnife; // knife: IKnife;
// } // }
interface INemesisWeakenResponse extends IKnifeResponse {
target: INemesisClient;
}
interface IKnife { interface IKnife {
Item: IEquipmentClient; Item: IEquipmentClient;
Skins: IWeaponSkinClient[]; Skins: IWeaponSkinClient[];
@ -323,3 +392,54 @@ interface IKnife {
AttachedUpgrades: IUpgradeClient[]; AttachedUpgrades: IUpgradeClient[];
HiddenWhenHolstered: boolean; HiddenWhenHolstered: boolean;
} }
const consumeModCharge = (
response: IKnifeResponse,
inventory: TInventoryDatabaseDocument,
upgrade: { ItemId: IOid; ItemType: string },
dataknifeUpgrades: string[]
): void => {
response.UpgradeIds ??= [];
response.UpgradeTypes ??= [];
response.UpgradeFingerprints ??= [];
response.UpgradeNew ??= [];
response.HasKnife = true;
if (upgrade.ItemId.$oid != "000000000000000000000000") {
const dbUpgrade = inventory.Upgrades.id(upgrade.ItemId.$oid)!;
const fingerprint = JSON.parse(dbUpgrade.UpgradeFingerprint!) as { lvl: number };
fingerprint.lvl += 1;
dbUpgrade.UpgradeFingerprint = JSON.stringify(fingerprint);
response.UpgradeIds.push(upgrade.ItemId.$oid);
response.UpgradeTypes.push(upgrade.ItemType);
response.UpgradeFingerprints.push(fingerprint);
response.UpgradeNew.push(false);
} else {
const id = new Types.ObjectId();
inventory.Upgrades.push({
_id: id,
ItemType: upgrade.ItemType,
UpgradeFingerprint: `{"lvl":1}`
});
addMods(inventory, [
{
ItemType: upgrade.ItemType,
ItemCount: -1
}
]);
const dataknifeRawUpgradeIndex = dataknifeUpgrades.indexOf(upgrade.ItemType);
if (dataknifeRawUpgradeIndex != -1) {
dataknifeUpgrades[dataknifeRawUpgradeIndex] = id.toString();
} else {
logger.warn(`${upgrade.ItemType} not found in dataknife config`);
}
response.UpgradeIds.push(id.toString());
response.UpgradeTypes.push(upgrade.ItemType);
response.UpgradeFingerprints.push({ lvl: 1 });
response.UpgradeNew.push(true);
}
};

View File

@ -57,7 +57,15 @@ export const placeDecoInComponentController: RequestHandler = async (req, res) =
component.DecoCapacity -= meta.capacityCost; component.DecoCapacity -= meta.capacityCost;
} }
} else { } else {
const itemType = Object.entries(ExportResources).find(arr => arr[1].deco == deco.Type)![0]; const entry = Object.entries(ExportResources).find(arr => arr[1].deco == deco.Type);
if (!entry) {
throw new Error(`unknown deco type: ${deco.Type}`);
}
const [itemType, meta] = entry;
if (meta.dojoCapacityCost === undefined) {
throw new Error(`unknown deco type: ${deco.Type}`);
}
component.DecoCapacity -= meta.dojoCapacityCost;
if (deco.Sockets !== undefined) { if (deco.Sockets !== undefined) {
guild.VaultFusionTreasures!.find(x => x.ItemType == itemType && x.Sockets == deco.Sockets)!.ItemCount -= guild.VaultFusionTreasures!.find(x => x.ItemType == itemType && x.Sockets == deco.Sockets)!.ItemCount -=
1; 1;
@ -71,7 +79,13 @@ export const placeDecoInComponentController: RequestHandler = async (req, res) =
if (meta) { if (meta) {
processDojoBuildMaterialsGathered(guild, meta); processDojoBuildMaterialsGathered(guild, meta);
} }
} else if (guild.AutoContributeFromVault && guild.VaultRegularCredits && guild.VaultMiscItems) { } else if (
deco.Type.startsWith("/Lotus/Objects/Tenno/Dojo/NpcPlaceables/") ||
(guild.AutoContributeFromVault && guild.VaultRegularCredits && guild.VaultMiscItems)
) {
if (!guild.VaultRegularCredits || !guild.VaultMiscItems) {
throw new Error(`dojo visitor placed without anything in vault?!`);
}
if (guild.VaultRegularCredits >= scaleRequiredCount(guild.Tier, meta.price)) { if (guild.VaultRegularCredits >= scaleRequiredCount(guild.Tier, meta.price)) {
let enoughMiscItems = true; let enoughMiscItems = true;
for (const ingredient of meta.ingredients) { for (const ingredient of meta.ingredients) {

View File

@ -1,25 +1,52 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory } from "@/src/services/inventoryService"; import { addConsumables, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { IPlayerSkills } from "@/src/types/inventoryTypes/inventoryTypes"; import { IPlayerSkills } from "@/src/types/inventoryTypes/inventoryTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
export const playerSkillsController: RequestHandler = async (req, res) => { export const playerSkillsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "PlayerSkills"); const inventory = await getInventory(accountId, "PlayerSkills Consumables");
const request = getJSONfromString<IPlayerSkillsRequest>(String(req.body)); const request = getJSONfromString<IPlayerSkillsRequest>(String(req.body));
const oldRank: number = inventory.PlayerSkills[request.Skill as keyof IPlayerSkills]; const oldRank: number = inventory.PlayerSkills[request.Skill as keyof IPlayerSkills];
const cost = (request.Pool == "LPP_DRIFTER" ? drifterCosts[oldRank] : 1 << oldRank) * 1000; const cost = (request.Pool == "LPP_DRIFTER" ? drifterCosts[oldRank] : 1 << oldRank) * 1000;
inventory.PlayerSkills[request.Pool as keyof IPlayerSkills] -= cost; inventory.PlayerSkills[request.Pool as keyof IPlayerSkills] -= cost;
inventory.PlayerSkills[request.Skill as keyof IPlayerSkills]++; inventory.PlayerSkills[request.Skill as keyof IPlayerSkills]++;
await inventory.save();
const inventoryChanges: IInventoryChanges = {};
if (request.Skill == "LPS_COMMAND") {
if (inventory.PlayerSkills.LPS_COMMAND == 9) {
const consumablesChanges = [
{
ItemType: "/Lotus/Types/Restoratives/Consumable/CrewmateBall",
ItemCount: 1
}
];
addConsumables(inventory, consumablesChanges);
inventoryChanges.Consumables = consumablesChanges;
}
} else if (request.Skill == "LPS_DRIFT_RIDING") {
if (inventory.PlayerSkills.LPS_DRIFT_RIDING == 9) {
const consumablesChanges = [
{
ItemType: "/Lotus/Types/Restoratives/ErsatzSummon",
ItemCount: 1
}
];
addConsumables(inventory, consumablesChanges);
inventoryChanges.Consumables = consumablesChanges;
}
}
await inventory.save();
res.json({ res.json({
Pool: request.Pool, Pool: request.Pool,
PoolInc: -cost, PoolInc: -cost,
Skill: request.Skill, Skill: request.Skill,
Rank: oldRank + 1 Rank: oldRank + 1,
InventoryChanges: inventoryChanges
}); });
}; };

View File

@ -11,7 +11,7 @@ export const projectionManagerController: RequestHandler = async (req, res) => {
const [era, category, currentQuality] = parseProjection(request.projectionType); const [era, category, currentQuality] = parseProjection(request.projectionType);
const upgradeCost = config.dontSubtractVoidTraces const upgradeCost = config.dontSubtractVoidTraces
? 0 ? 0
: (request.qualityTag - qualityKeywordToNumber[currentQuality]) * 25; : qualityNumberToCost[request.qualityTag] - qualityNumberToCost[qualityKeywordToNumber[currentQuality]];
const newProjectionType = findProjection(era, category, qualityNumberToKeyword[request.qualityTag]); const newProjectionType = findProjection(era, category, qualityNumberToKeyword[request.qualityTag]);
addMiscItems(inventory, [ addMiscItems(inventory, [
{ {
@ -49,6 +49,7 @@ const qualityKeywordToNumber: Record<VoidProjectionQuality, number> = {
VPQ_GOLD: 2, VPQ_GOLD: 2,
VPQ_PLATINUM: 3 VPQ_PLATINUM: 3
}; };
const qualityNumberToCost = [0, 25, 50, 100];
// e.g. "/Lotus/Types/Game/Projections/T2VoidProjectionProteaPrimeDBronze" -> ["Lith", "W5", "VPQ_BRONZE"] // e.g. "/Lotus/Types/Game/Projections/T2VoidProjectionProteaPrimeDBronze" -> ["Lith", "W5", "VPQ_BRONZE"]
const parseProjection = (typeName: string): [string, string, VoidProjectionQuality] => { const parseProjection = (typeName: string): [string, string, VoidProjectionQuality] => {

View File

@ -3,6 +3,7 @@ import { getAccountIdForRequest } from "@/src/services/loginService";
import { IPurchaseRequest } from "@/src/types/purchaseTypes"; import { IPurchaseRequest } from "@/src/types/purchaseTypes";
import { handlePurchase } from "@/src/services/purchaseService"; import { handlePurchase } from "@/src/services/purchaseService";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { sendWsBroadcastTo } from "@/src/services/wsService";
export const purchaseController: RequestHandler = async (req, res) => { export const purchaseController: RequestHandler = async (req, res) => {
const purchaseRequest = JSON.parse(String(req.body)) as IPurchaseRequest; const purchaseRequest = JSON.parse(String(req.body)) as IPurchaseRequest;
@ -10,5 +11,7 @@ export const purchaseController: RequestHandler = async (req, res) => {
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const response = await handlePurchase(purchaseRequest, inventory); const response = await handlePurchase(purchaseRequest, inventory);
await inventory.save(); await inventory.save();
//console.log(JSON.stringify(response, null, 2));
res.json(response); res.json(response);
sendWsBroadcastTo(accountId, { update_inventory: true });
}; };

View File

@ -1,6 +1,7 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory, updateCurrency } from "@/src/services/inventoryService"; import { getInventory, updateCurrency } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { sendWsBroadcastTo } from "@/src/services/wsService";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
export const releasePetController: RequestHandler = async (req, res) => { export const releasePetController: RequestHandler = async (req, res) => {
@ -19,6 +20,7 @@ export const releasePetController: RequestHandler = async (req, res) => {
await inventory.save(); await inventory.save();
res.json({ inventoryChanges }); // Not a mistake; it's "inventoryChanges" here. res.json({ inventoryChanges }); // Not a mistake; it's "inventoryChanges" here.
sendWsBroadcastTo(accountId, { update_inventory: true });
}; };
interface IReleasePetRequest { interface IReleasePetRequest {

View File

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

View File

@ -0,0 +1,5 @@
import { RequestHandler } from "express";
export const resetQuestProgressController: RequestHandler = (_req, res) => {
res.send("1").end();
};

View File

@ -1,7 +1,7 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { Status } from "@/src/types/inventoryTypes/inventoryTypes"; import { Status } from "@/src/types/equipmentTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
export const retrievePetFromStasisController: RequestHandler = async (req, res) => { export const retrievePetFromStasisController: RequestHandler = async (req, res) => {

View File

@ -24,7 +24,7 @@ export const saveDialogueController: RequestHandler = async (req, res) => {
inventory.DialogueHistory.Dialogues ??= []; inventory.DialogueHistory.Dialogues ??= [];
const dialogue = getDialogue(inventory, request.DialogueName); const dialogue = getDialogue(inventory, request.DialogueName);
dialogue.Rank = request.Rank; dialogue.Rank = request.Rank;
dialogue.Chemistry = request.Chemistry; dialogue.Chemistry += request.Chemistry;
dialogue.QueuedDialogues = request.QueuedDialogues; dialogue.QueuedDialogues = request.QueuedDialogues;
for (const bool of request.Booleans) { for (const bool of request.Booleans) {
dialogue.Booleans.push(bool); dialogue.Booleans.push(bool);

View File

@ -2,7 +2,7 @@ import { getAccountIdForRequest } from "@/src/services/loginService";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { ISettings } from "../../types/inventoryTypes/inventoryTypes"; import { ISettings } from "@/src/types/inventoryTypes/inventoryTypes";
interface ISaveSettingsRequest { interface ISaveSettingsRequest {
Settings: ISettings; Settings: ISettings;

View File

@ -9,21 +9,28 @@ import {
freeUpSlot, freeUpSlot,
combineInventoryChanges, combineInventoryChanges,
addCrewShipRawSalvage, addCrewShipRawSalvage,
addFusionPoints addFusionPoints,
addCrewShipFusionPoints,
addFusionTreasures
} from "@/src/services/inventoryService"; } from "@/src/services/inventoryService";
import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
import { ExportDojoRecipes } from "warframe-public-export-plus"; import { ExportDojoRecipes } from "warframe-public-export-plus";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { sendWsBroadcastEx } from "@/src/services/wsService";
import { parseFusionTreasure } from "@/src/helpers/inventoryHelpers";
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;
//console.log(JSON.stringify(payload, null, 2));
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const requiredFields = new Set<keyof TInventoryDatabaseDocument>(); const requiredFields = new Set<keyof TInventoryDatabaseDocument>();
if (payload.SellCurrency == "SC_RegularCredits") { if (payload.SellCurrency == "SC_RegularCredits") {
requiredFields.add("RegularCredits"); requiredFields.add("RegularCredits");
} else if (payload.SellCurrency == "SC_FusionPoints") { } else if (payload.SellCurrency == "SC_FusionPoints") {
requiredFields.add("FusionPoints"); requiredFields.add("FusionPoints");
} else if (payload.SellCurrency == "SC_CrewShipFusionPoints") {
requiredFields.add("CrewShipFusionPoints");
} else { } else {
requiredFields.add("MiscItems"); requiredFields.add("MiscItems");
} }
@ -57,6 +64,9 @@ export const sellController: RequestHandler = async (req, res) => {
if (payload.Items.Hoverboards) { if (payload.Items.Hoverboards) {
requiredFields.add(InventorySlot.SPACESUITS); requiredFields.add(InventorySlot.SPACESUITS);
} }
if (payload.Items.CrewMembers) {
requiredFields.add(InventorySlot.CREWMEMBERS);
}
if (payload.Items.CrewShipWeapons || payload.Items.CrewShipWeaponSkins) { if (payload.Items.CrewShipWeapons || payload.Items.CrewShipWeaponSkins) {
requiredFields.add(InventorySlot.RJ_COMPONENT_AND_ARMAMENTS); requiredFields.add(InventorySlot.RJ_COMPONENT_AND_ARMAMENTS);
requiredFields.add("CrewShipRawSalvage"); requiredFields.add("CrewShipRawSalvage");
@ -74,6 +84,8 @@ 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") {
addFusionPoints(inventory, payload.SellPrice); addFusionPoints(inventory, payload.SellPrice);
} else if (payload.SellCurrency == "SC_CrewShipFusionPoints") {
addCrewShipFusionPoints(inventory, payload.SellPrice);
} else if (payload.SellCurrency == "SC_PrimeBucks") { } else if (payload.SellCurrency == "SC_PrimeBucks") {
addMiscItems(inventory, [ addMiscItems(inventory, [
{ {
@ -180,6 +192,17 @@ export const sellController: RequestHandler = async (req, res) => {
inventory.Drones.pull({ _id: sellItem.String }); inventory.Drones.pull({ _id: sellItem.String });
}); });
} }
if (payload.Items.KubrowPetPrints) {
payload.Items.KubrowPetPrints.forEach(sellItem => {
inventory.KubrowPetPrints.pull({ _id: sellItem.String });
});
}
if (payload.Items.CrewMembers) {
payload.Items.CrewMembers.forEach(sellItem => {
inventory.CrewMembers.pull({ _id: sellItem.String });
freeUpSlot(inventory, InventorySlot.CREWMEMBERS);
});
}
if (payload.Items.CrewShipWeapons) { if (payload.Items.CrewShipWeapons) {
payload.Items.CrewShipWeapons.forEach(sellItem => { payload.Items.CrewShipWeapons.forEach(sellItem => {
if (sellItem.String[0] == "/") { if (sellItem.String[0] == "/") {
@ -274,11 +297,17 @@ export const sellController: RequestHandler = async (req, res) => {
]); ]);
}); });
} }
if (payload.Items.FusionTreasures) {
payload.Items.FusionTreasures.forEach(sellItem => {
addFusionTreasures(inventory, [parseFusionTreasure(sellItem.String, sellItem.Count * -1)]);
});
}
await inventory.save(); await inventory.save();
res.json({ res.json({
inventoryChanges: inventoryChanges // "inventoryChanges" for this response instead of the usual "InventoryChanges" inventoryChanges: inventoryChanges // "inventoryChanges" for this response instead of the usual "InventoryChanges"
}); });
sendWsBroadcastEx({ update_inventory: true }, accountId, parseInt(String(req.query.wsid)));
}; };
interface ISellRequest { interface ISellRequest {
@ -301,8 +330,11 @@ interface ISellRequest {
OperatorAmps?: ISellItem[]; OperatorAmps?: ISellItem[];
Hoverboards?: ISellItem[]; Hoverboards?: ISellItem[];
Drones?: ISellItem[]; Drones?: ISellItem[];
KubrowPetPrints?: ISellItem[];
CrewMembers?: ISellItem[];
CrewShipWeapons?: ISellItem[]; CrewShipWeapons?: ISellItem[];
CrewShipWeaponSkins?: ISellItem[]; CrewShipWeaponSkins?: ISellItem[];
FusionTreasures?: ISellItem[];
}; };
SellPrice: number; SellPrice: number;
SellCurrency: SellCurrency:
@ -311,7 +343,8 @@ interface ISellRequest {
| "SC_FusionPoints" | "SC_FusionPoints"
| "SC_DistillPoints" | "SC_DistillPoints"
| "SC_CrewShipFusionPoints" | "SC_CrewShipFusionPoints"
| "SC_Resources"; | "SC_Resources"
| "somethingelsewemightnotknowabout";
buildLabel: string; buildLabel: string;
} }

View File

@ -1,7 +1,7 @@
import { RequestHandler } from "express"; 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/personalRoomsTypes";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
export const setBootLocationController: RequestHandler = async (req, res) => { export const setBootLocationController: RequestHandler = async (req, res) => {

View File

@ -1,5 +1,5 @@
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { IPictureFrameInfo, ISetPlacedDecoInfoRequest } from "@/src/types/shipTypes"; import { IPictureFrameInfo, ISetPlacedDecoInfoRequest } from "@/src/types/personalRoomsTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { handleSetPlacedDecoInfo } from "@/src/services/shipCustomizationsService"; import { handleSetPlacedDecoInfo } from "@/src/services/shipCustomizationsService";

View File

@ -1,6 +1,6 @@
import { getAccountIdForRequest } from "@/src/services/loginService"; 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/personalRoomsTypes";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express"; import { RequestHandler } from "express";

View File

@ -3,7 +3,7 @@ import { RequestHandler } from "express";
import { getPersonalRooms } from "@/src/services/personalRoomsService"; import { getPersonalRooms } from "@/src/services/personalRoomsService";
import { IOid } from "@/src/types/commonTypes"; import { IOid } from "@/src/types/commonTypes";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { IFavouriteLoadoutDatabase, TBootLocation } from "@/src/types/shipTypes"; import { IFavouriteLoadoutDatabase, TBootLocation } from "@/src/types/personalRoomsTypes";
export const setShipFavouriteLoadoutController: RequestHandler = async (req, res) => { export const setShipFavouriteLoadoutController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);

View File

@ -0,0 +1,22 @@
import { fromMongoDate, fromOid } from "@/src/helpers/inventoryHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IEquipmentClient } from "@/src/types/equipmentTypes";
import { RequestHandler } from "express";
export const setSuitInfectionController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "Suits");
const payload = getJSONfromString<ISetSuitInfectionRequest>(String(req.body));
for (const clientSuit of payload.Suits) {
const dbSuit = inventory.Suits.id(fromOid(clientSuit.ItemId))!;
dbSuit.InfestationDate = fromMongoDate(clientSuit.InfestationDate!);
}
await inventory.save();
res.end();
};
interface ISetSuitInfectionRequest {
Suits: IEquipmentClient[];
}

View File

@ -1,20 +1,17 @@
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { IShipDecorationsRequest } from "@/src/types/shipTypes"; import { IShipDecorationsRequest, IResetShipDecorationsRequest } from "@/src/types/personalRoomsTypes";
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { handleSetShipDecorations } from "@/src/services/shipCustomizationsService"; import { handleResetShipDecorations, handleSetShipDecorations } from "@/src/services/shipCustomizationsService";
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; if (req.query.reset == "1") {
const request = JSON.parse(req.body as string) as IResetShipDecorationsRequest;
try { const response = await handleResetShipDecorations(accountId, request);
res.send(response);
} else {
const shipDecorationsRequest = JSON.parse(req.body as string) as IShipDecorationsRequest;
const placedDecoration = await handleSetShipDecorations(accountId, shipDecorationsRequest); const placedDecoration = await handleSetShipDecorations(accountId, shipDecorationsRequest);
res.send(placedDecoration); res.send(placedDecoration);
} catch (error: unknown) {
if (error instanceof Error) {
logger.error(`error in shipDecorationsController: ${error.message}`);
res.status(400).json({ error: error.message });
}
} }
}; };

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,27 @@
import { fromMongoDate, fromOid } from "@/src/helpers/inventoryHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { addMiscItem, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IEquipmentClient } from "@/src/types/equipmentTypes";
import { RequestHandler } from "express";
export const umbraController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "Suits MiscItems");
const payload = getJSONfromString<IUmbraRequest>(String(req.body));
for (const clientSuit of payload.Suits) {
const dbSuit = inventory.Suits.id(fromOid(clientSuit.ItemId))!;
if (clientSuit.UmbraDate) {
addMiscItem(inventory, "/Lotus/Types/Items/MiscItems/UmbraEchoes", -1);
dbSuit.UmbraDate = fromMongoDate(clientSuit.UmbraDate);
} else {
dbSuit.UmbraDate = undefined;
}
}
await inventory.save();
res.end();
};
interface IUmbraRequest {
Suits: IEquipmentClient[];
}

View File

@ -1,36 +1,63 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getAccountForRequest } from "@/src/services/loginService"; import { getAccountForRequest } from "@/src/services/loginService";
import { addChallenges, getInventory } from "@/src/services/inventoryService"; import { addCalendarProgress, addChallenges, getInventory } from "@/src/services/inventoryService";
import { IChallengeProgress, ISeasonChallenge } from "@/src/types/inventoryTypes/inventoryTypes"; import { IChallengeProgress, ISeasonChallenge } from "@/src/types/inventoryTypes/inventoryTypes";
import { IAffiliationMods } from "@/src/types/purchaseTypes"; import { IAffiliationMods } from "@/src/types/purchaseTypes";
import { getEntriesUnsafe } from "@/src/utils/ts-utils";
import { logger } from "@/src/utils/logger";
export const updateChallengeProgressController: RequestHandler = async (req, res) => { export const updateChallengeProgressController: RequestHandler = async (req, res) => {
const challenges = getJSONfromString<IUpdateChallengeProgressRequest>(String(req.body)); const challenges = getJSONfromString<IUpdateChallengeProgressRequest>(String(req.body));
const account = await getAccountForRequest(req); const account = await getAccountForRequest(req);
logger.debug(`challenge report:`, challenges);
const inventory = await getInventory( const inventory = await getInventory(
account._id.toString(), account._id.toString(),
"ChallengeProgress SeasonChallengeHistory Affiliations" "ChallengesFixVersion ChallengeProgress SeasonChallengeHistory Affiliations CalendarProgress"
); );
let affiliationMods: IAffiliationMods[] = []; let affiliationMods: IAffiliationMods[] = [];
if (challenges.ChallengeProgress) { if (challenges.ChallengeProgress) {
affiliationMods = addChallenges( affiliationMods = await addChallenges(
account, account,
inventory, inventory,
challenges.ChallengeProgress, challenges.ChallengeProgress,
challenges.SeasonChallengeCompletions challenges.SeasonChallengeCompletions
); );
} }
if (challenges.SeasonChallengeHistory) { for (const [key, value] of getEntriesUnsafe(challenges)) {
challenges.SeasonChallengeHistory.forEach(({ challenge, id }) => { if (value === undefined) {
const itemIndex = inventory.SeasonChallengeHistory.findIndex(i => i.challenge === challenge); logger.error(`Challenge progress update key ${key} has no value`);
if (itemIndex !== -1) { continue;
inventory.SeasonChallengeHistory[itemIndex].id = id; }
} else { switch (key) {
inventory.SeasonChallengeHistory.push({ challenge, id }); case "ChallengesFixVersion":
} inventory.ChallengesFixVersion = value;
}); break;
case "SeasonChallengeHistory":
value.forEach(({ challenge, id }) => {
const itemIndex = inventory.SeasonChallengeHistory.findIndex(i => i.challenge === challenge);
if (itemIndex !== -1) {
inventory.SeasonChallengeHistory[itemIndex].id = id;
} else {
inventory.SeasonChallengeHistory.push({ challenge, id });
}
});
break;
case "CalendarProgress":
addCalendarProgress(inventory, value);
break;
case "ChallengeProgress":
case "SeasonChallengeCompletions":
case "ChallengePTS":
case "crossPlaySetting":
break;
default:
logger.warn(`unknown challenge progress entry`, { key, value });
}
} }
await inventory.save(); await inventory.save();
@ -40,7 +67,11 @@ export const updateChallengeProgressController: RequestHandler = async (req, res
}; };
interface IUpdateChallengeProgressRequest { interface IUpdateChallengeProgressRequest {
ChallengePTS?: number;
ChallengesFixVersion?: number;
ChallengeProgress?: IChallengeProgress[]; ChallengeProgress?: IChallengeProgress[];
SeasonChallengeHistory?: ISeasonChallenge[]; SeasonChallengeHistory?: ISeasonChallenge[];
SeasonChallengeCompletions?: ISeasonChallenge[]; SeasonChallengeCompletions?: ISeasonChallenge[];
CalendarProgress?: { challenge: string }[];
crossPlaySetting?: string;
} }

View File

@ -1,11 +1,6 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { IUpgradesRequest } from "@/src/types/requestTypes"; import { IUpgradesRequest } from "@/src/types/requestTypes";
import { import { ArtifactPolarity, IAbilityOverride } from "@/src/types/inventoryTypes/commonInventoryTypes";
ArtifactPolarity,
IEquipmentDatabase,
EquipmentFeatures,
IAbilityOverride
} from "@/src/types/inventoryTypes/commonInventoryTypes";
import { IInventoryClient, 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, addRecipes, getInventory, updateCurrency } from "@/src/services/inventoryService"; import { addMiscItems, addRecipes, getInventory, updateCurrency } from "@/src/services/inventoryService";
@ -13,6 +8,8 @@ import { getRecipeByResult } from "@/src/services/itemDataService";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { addInfestedFoundryXP, applyCheatsToInfestedFoundry } from "@/src/services/infestedFoundryService"; import { addInfestedFoundryXP, applyCheatsToInfestedFoundry } from "@/src/services/infestedFoundryService";
import { config } from "@/src/services/configService"; import { config } from "@/src/services/configService";
import { sendWsBroadcastTo } from "@/src/services/wsService";
import { EquipmentFeatures, IEquipmentDatabase } from "@/src/types/equipmentTypes";
export const upgradesController: RequestHandler = async (req, res) => { export const upgradesController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
@ -120,6 +117,7 @@ export const upgradesController: RequestHandler = async (req, res) => {
setSlotPolarity(item, operation.PolarizeSlot, operation.PolarizeValue); setSlotPolarity(item, operation.PolarizeSlot, operation.PolarizeValue);
item.Polarized ??= 0; item.Polarized ??= 0;
item.Polarized += 1; item.Polarized += 1;
sendWsBroadcastTo(accountId, { update_inventory: true }); // webui may need to to re-add "max rank" button
break; break;
} }
case "/Lotus/Types/Items/MiscItems/ModSlotUnlocker": { case "/Lotus/Types/Items/MiscItems/ModSlotUnlocker": {

View File

@ -0,0 +1,33 @@
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express";
export const abilityOverrideController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const request = req.body as IAbilityOverrideRequest;
if (request.category === "Suits") {
const inventory = await getInventory(accountId, request.category);
const item = inventory[request.category].id(request.oid);
if (item) {
if (request.action == "set") {
item.Configs[request.configIndex].AbilityOverride = request.AbilityOverride;
} else {
item.Configs[request.configIndex].AbilityOverride = undefined;
}
await inventory.save();
}
}
res.end();
};
interface IAbilityOverrideRequest {
category: TEquipmentKey;
oid: string;
action: "set" | "remove";
configIndex: number;
AbilityOverride: {
Ability: string;
Index: number;
};
}

View File

@ -7,7 +7,7 @@ export const addItemsController: RequestHandler = async (req, res) => {
const requests = req.body as IAddItemRequest[]; const requests = req.body as IAddItemRequest[];
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
for (const request of requests) { for (const request of requests) {
await addItem(inventory, request.ItemType, request.ItemCount, true, undefined, undefined, true); await addItem(inventory, request.ItemType, request.ItemCount, true, undefined, request.Fingerprint, true);
} }
await inventory.save(); await inventory.save();
res.end(); res.end();
@ -16,4 +16,5 @@ export const addItemsController: RequestHandler = async (req, res) => {
interface IAddItemRequest { interface IAddItemRequest {
ItemType: string; ItemType: string;
ItemCount: number; ItemCount: number;
Fingerprint?: string;
} }

View File

@ -0,0 +1,24 @@
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory, addRecipes } from "@/src/services/inventoryService";
import { RequestHandler } from "express";
import { ExportRecipes } from "warframe-public-export-plus";
export const addMissingHelminthBlueprintsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "Recipes");
const allHelminthRecipes = Object.keys(ExportRecipes).filter(
key => ExportRecipes[key].secretIngredientAction === "SIA_WARFRAME_ABILITY"
);
const inventoryHelminthRecipes = inventory.Recipes.filter(recipe =>
recipe.ItemType.startsWith("/Lotus/Types/Recipes/AbilityOverrides/")
).map(recipe => recipe.ItemType);
const missingHelminthRecipes = allHelminthRecipes
.filter(key => !inventoryHelminthRecipes.includes(key))
.map(ItemType => ({ ItemType, ItemCount: 1 }));
addRecipes(inventory, missingHelminthRecipes);
await inventory.save();
res.end();
};

View File

@ -1,7 +1,7 @@
import { applyClientEquipmentUpdates, getInventory } from "@/src/services/inventoryService"; import { applyClientEquipmentUpdates, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { IOid } from "@/src/types/commonTypes"; import { IOid } from "@/src/types/commonTypes";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { IEquipmentClient } from "@/src/types/equipmentTypes";
import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { ExportMisc } from "warframe-public-export-plus"; import { ExportMisc } from "warframe-public-export-plus";

View File

@ -0,0 +1,65 @@
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express";
export const changeModularPartsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const request = req.body as IUpdateFingerPrintRequest;
const inventory = await getInventory(accountId, request.category);
const item = inventory[request.category].id(request.oid);
if (item) {
item.ModularParts = request.modularParts;
request.modularParts.forEach(part => {
const categoryMap = mapping[part];
if (categoryMap && categoryMap[request.category]) {
item.ItemType = categoryMap[request.category]!;
}
});
await inventory.save();
}
res.end();
};
interface IUpdateFingerPrintRequest {
category: TEquipmentKey;
oid: string;
modularParts: string[];
}
const mapping: Partial<Record<string, Partial<Record<TEquipmentKey, string>>>> = {
"/Lotus/Weapons/SolarisUnited/Secondary/SUModularSecondarySet1/Barrel/SUModularSecondaryBarrelAPart": {
LongGuns: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryShotgun",
Pistols: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryShotgun"
},
"/Lotus/Weapons/Infested/Pistols/InfKitGun/Barrels/InfBarrelEgg/InfModularBarrelEggPart": {
LongGuns: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryShotgun",
Pistols: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryShotgun"
},
"/Lotus/Weapons/SolarisUnited/Secondary/SUModularSecondarySet1/Barrel/SUModularSecondaryBarrelBPart": {
LongGuns: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary",
Pistols: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary"
},
"/Lotus/Weapons/SolarisUnited/Secondary/SUModularSecondarySet1/Barrel/SUModularSecondaryBarrelCPart": {
LongGuns: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary",
Pistols: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary"
},
"/Lotus/Weapons/SolarisUnited/Secondary/SUModularSecondarySet1/Barrel/SUModularSecondaryBarrelDPart": {
LongGuns: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryBeam",
Pistols: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryBeam"
},
"/Lotus/Weapons/Infested/Pistols/InfKitGun/Barrels/InfBarrelBeam/InfModularBarrelBeamPart": {
LongGuns: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryBeam",
Pistols: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryBeam"
},
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadA": {
MoaPets: "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetAPowerSuit"
},
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadB": {
MoaPets: "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetBPowerSuit"
},
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadC": {
MoaPets: "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetCPowerSuit"
}
};

View File

@ -0,0 +1,40 @@
import { addString } from "@/src/helpers/stringHelpers";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { addFixedLevelRewards } from "@/src/services/missionInventoryUpdateService";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { IMissionReward } from "@/src/types/missionTypes";
import { RequestHandler } from "express";
import { ExportRegions } from "warframe-public-export-plus";
export const completeAllMissionsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const MissionRewards: IMissionReward[] = [];
for (const [tag, node] of Object.entries(ExportRegions)) {
let mission = inventory.Missions.find(x => x.Tag == tag);
if (!mission) {
mission =
inventory.Missions[
inventory.Missions.push({
Completes: 0,
Tier: 0,
Tag: tag
}) - 1
];
}
if (mission.Completes == 0) {
mission.Completes++;
if (node.missionReward) {
addFixedLevelRewards(node.missionReward, MissionRewards);
}
}
mission.Tier = 1;
}
for (const reward of MissionRewards) {
await handleStoreItemAcquisition(reward.StoreItem, inventory, reward.ItemCount, undefined, true);
}
addString(inventory.NodeIntrosCompleted, "TeshinHardModeUnlocked");
await inventory.save();
res.end();
};

View File

@ -0,0 +1,47 @@
import { RequestHandler } from "express";
import { config, syncConfigWithDatabase } from "@/src/services/configService";
import { getAccountForRequest, isAdministrator } from "@/src/services/loginService";
import { saveConfig } from "@/src/services/configWriterService";
import { sendWsBroadcastEx } from "@/src/services/wsService";
export const getConfigController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req);
if (isAdministrator(account)) {
const responseData: Record<string, boolean | string | number | null> = {};
for (const id of req.body as string[]) {
const [obj, idx] = configIdToIndexable(id);
responseData[id] = obj[idx] ?? null;
}
res.json(responseData);
} else {
res.status(401).end();
}
};
export const setConfigController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req);
if (isAdministrator(account)) {
for (const [id, value] of Object.entries(req.body as Record<string, boolean | string | number>)) {
const [obj, idx] = configIdToIndexable(id);
obj[idx] = value;
}
sendWsBroadcastEx({ config_reloaded: true }, undefined, parseInt(String(req.query.wsid)));
syncConfigWithDatabase();
await saveConfig();
res.end();
} else {
res.status(401).end();
}
};
const configIdToIndexable = (id: string): [Record<string, boolean | string | number | undefined>, string] => {
let obj = config as unknown as Record<string, never>;
const arr = id.split(".");
while (arr.length > 1) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
obj[arr[0]] ??= {} as never;
obj = obj[arr[0]];
arr.splice(0, 1);
}
return [obj, arr[0]];
};

View File

@ -11,6 +11,7 @@ import { GuildMember } from "@/src/models/guildModel";
import { Leaderboard } from "@/src/models/leaderboardModel"; import { Leaderboard } from "@/src/models/leaderboardModel";
import { deleteGuild } from "@/src/services/guildService"; import { deleteGuild } from "@/src/services/guildService";
import { Friendship } from "@/src/models/friendModel"; import { Friendship } from "@/src/models/friendModel";
import { sendWsBroadcastTo } from "@/src/services/wsService";
export const deleteAccountController: RequestHandler = async (req, res) => { export const deleteAccountController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
@ -36,5 +37,8 @@ export const deleteAccountController: RequestHandler = async (req, res) => {
Ship.deleteMany({ ShipOwnerId: accountId }), Ship.deleteMany({ ShipOwnerId: accountId }),
Stats.deleteOne({ accountOwnerId: accountId }) Stats.deleteOne({ accountOwnerId: accountId })
]); ]);
sendWsBroadcastTo(accountId, { logged_out: true });
res.end(); res.end();
}; };

View File

@ -0,0 +1,34 @@
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory } from "@/src/services/inventoryService";
import { RequestHandler } from "express";
const DEFAULT_UPGRADE_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
export const editSuitInvigorationUpgradeController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const { oid, data } = req.body as {
oid: string;
data?: {
DefensiveUpgrade: string;
OffensiveUpgrade: string;
UpgradesExpiry?: number;
};
};
const inventory = await getInventory(accountId);
const suit = inventory.Suits.id(oid)!;
if (data) {
suit.DefensiveUpgrade = data.DefensiveUpgrade;
suit.OffensiveUpgrade = data.OffensiveUpgrade;
if (data.UpgradesExpiry) {
suit.UpgradesExpiry = new Date(data.UpgradesExpiry);
} else {
suit.UpgradesExpiry = new Date(Date.now() + DEFAULT_UPGRADE_EXPIRY_MS);
}
} else {
suit.DefensiveUpgrade = undefined;
suit.OffensiveUpgrade = undefined;
suit.UpgradesExpiry = undefined;
}
await inventory.save();
res.end();
};

View File

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

View File

@ -1,8 +1,10 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getDict, getItemName, getString } from "@/src/services/itemDataService"; import { getDict, getItemName, getString } from "@/src/services/itemDataService";
import { import {
ExportAbilities,
ExportArcanes, ExportArcanes,
ExportAvionics, ExportAvionics,
ExportBoosters,
ExportCustoms, ExportCustoms,
ExportDrones, ExportDrones,
ExportGear, ExportGear,
@ -19,12 +21,13 @@ import {
ExportWeapons, ExportWeapons,
TRelicQuality TRelicQuality
} from "warframe-public-export-plus"; } from "warframe-public-export-plus";
import archonCrystalUpgrades from "@/static/fixed_responses/webuiArchonCrystalUpgrades.json";
import allIncarnons from "@/static/fixed_responses/allIncarnonList.json"; import allIncarnons from "@/static/fixed_responses/allIncarnonList.json";
import varzia from "@/static/fixed_responses/worldState/varzia.json";
interface ListedItem { interface ListedItem {
uniqueName: string; uniqueName: string;
name: string; name: string;
subtype?: string;
fusionLimit?: number; fusionLimit?: number;
exalted?: string[]; exalted?: string[];
badReason?: "starter" | "frivolous" | "notraw"; badReason?: "starter" | "frivolous" | "notraw";
@ -34,7 +37,6 @@ interface ListedItem {
} }
interface ItemLists { interface ItemLists {
archonCrystalUpgrades: Record<string, string>;
uniqueLevelCaps: Record<string, number>; uniqueLevelCaps: Record<string, number>;
Suits: ListedItem[]; Suits: ListedItem[];
LongGuns: ListedItem[]; LongGuns: ListedItem[];
@ -54,6 +56,10 @@ interface ItemLists {
KubrowPets: ListedItem[]; KubrowPets: ListedItem[];
EvolutionProgress: ListedItem[]; EvolutionProgress: ListedItem[];
mods: ListedItem[]; mods: ListedItem[];
Boosters: ListedItem[];
VarziaOffers: ListedItem[];
Abilities: ListedItem[];
//circuitGameModes: ListedItem[];
} }
const relicQualitySuffixes: Record<TRelicQuality, string> = { const relicQualitySuffixes: Record<TRelicQuality, string> = {
@ -63,10 +69,13 @@ const relicQualitySuffixes: Record<TRelicQuality, string> = {
VPQ_PLATINUM: " [Exceptional]" VPQ_PLATINUM: " [Exceptional]"
}; };
/*const toTitleCase = (str: string): string => {
return str.replace(/[^\s-]+/g, word => word.charAt(0).toUpperCase() + word.substr(1).toLowerCase());
};*/
const getItemListsController: RequestHandler = (req, response) => { const getItemListsController: RequestHandler = (req, response) => {
const lang = getDict(typeof req.query.lang == "string" ? req.query.lang : "en"); const lang = getDict(typeof req.query.lang == "string" ? req.query.lang : "en");
const res: ItemLists = { const res: ItemLists = {
archonCrystalUpgrades,
uniqueLevelCaps: ExportMisc.uniqueLevelCaps, uniqueLevelCaps: ExportMisc.uniqueLevelCaps,
Suits: [], Suits: [],
LongGuns: [], LongGuns: [],
@ -85,7 +94,40 @@ const getItemListsController: RequestHandler = (req, response) => {
QuestKeys: [], QuestKeys: [],
KubrowPets: [], KubrowPets: [],
EvolutionProgress: [], EvolutionProgress: [],
mods: [] mods: [],
Boosters: [],
VarziaOffers: [],
Abilities: []
/*circuitGameModes: [
{
uniqueName: "Survival",
name: toTitleCase(getString("/Lotus/Language/Missions/MissionName_Survival", lang))
},
{
uniqueName: "VoidFlood",
name: toTitleCase(getString("/Lotus/Language/Missions/MissionName_Corruption", lang))
},
{
uniqueName: "Excavation",
name: toTitleCase(getString("/Lotus/Language/Missions/MissionName_Excavation", lang))
},
{
uniqueName: "Defense",
name: toTitleCase(getString("/Lotus/Language/Missions/MissionName_Defense", lang))
},
{
uniqueName: "Exterminate",
name: toTitleCase(getString("/Lotus/Language/Missions/MissionName_Exterminate", lang))
},
{
uniqueName: "Assassination",
name: toTitleCase(getString("/Lotus/Language/Missions/MissionName_Assassination", lang))
},
{
uniqueName: "Alchemy",
name: toTitleCase(getString("/Lotus/Language/Missions/MissionName_Alchemy", lang))
}
]*/
}; };
for (const [uniqueName, item] of Object.entries(ExportWarframes)) { for (const [uniqueName, item] of Object.entries(ExportWarframes)) {
res[item.productCategory].push({ res[item.productCategory].push({
@ -93,6 +135,12 @@ const getItemListsController: RequestHandler = (req, response) => {
name: getString(item.name, lang), name: getString(item.name, lang),
exalted: item.exalted exalted: item.exalted
}); });
item.abilities.forEach(ability => {
res.Abilities.push({
uniqueName: ability.uniqueName,
name: getString(ability.name || uniqueName, lang)
});
});
} }
for (const [uniqueName, item] of Object.entries(ExportSentinels)) { for (const [uniqueName, item] of Object.entries(ExportSentinels)) {
if (item.productCategory == "Sentinels" || item.productCategory == "KubrowPets") { if (item.productCategory == "Sentinels" || item.productCategory == "KubrowPets") {
@ -105,18 +153,21 @@ const getItemListsController: RequestHandler = (req, response) => {
} }
for (const [uniqueName, item] of Object.entries(ExportWeapons)) { for (const [uniqueName, item] of Object.entries(ExportWeapons)) {
if (item.partType) { if (item.partType) {
if (!uniqueName.startsWith("/Lotus/Types/Items/Deimos/")) { if (!uniqueName.split("/")[7]?.startsWith("PvPVariant")) {
res.ModularParts.push({ // not a pvp variant
uniqueName, if (!uniqueName.startsWith("/Lotus/Types/Items/Deimos/")) {
name: getString(item.name, lang), res.ModularParts.push({
partType: item.partType uniqueName,
}); name: getString(item.name, lang),
} partType: item.partType
if (uniqueName.split("/")[5] != "SentTrainingAmplifier") { });
res.miscitems.push({ }
uniqueName: uniqueName, if (uniqueName.split("/")[5] != "SentTrainingAmplifier") {
name: getString(item.name, lang) res.miscitems.push({
}); uniqueName: uniqueName,
name: getString(item.name, lang)
});
}
} }
} else if (item.totalDamage !== 0) { } else if (item.totalDamage !== 0) {
if ( if (
@ -175,7 +226,8 @@ const getItemListsController: RequestHandler = (req, response) => {
) { ) {
res.miscitems.push({ res.miscitems.push({
uniqueName: uniqueName, uniqueName: uniqueName,
name: name name: name,
subtype: "Resource"
}); });
} }
} }
@ -193,7 +245,8 @@ const getItemListsController: RequestHandler = (req, response) => {
for (const [uniqueName, item] of Object.entries(ExportGear)) { for (const [uniqueName, item] of Object.entries(ExportGear)) {
res.miscitems.push({ res.miscitems.push({
uniqueName: uniqueName, uniqueName: uniqueName,
name: getString(item.name, lang) name: getString(item.name, lang),
subtype: "Gear"
}); });
} }
const recipeNameTemplate = getString("/Lotus/Language/Items/BlueprintAndItem", lang); const recipeNameTemplate = getString("/Lotus/Language/Items/BlueprintAndItem", lang);
@ -293,6 +346,27 @@ const getItemListsController: RequestHandler = (req, response) => {
}); });
} }
for (const item of Object.values(ExportBoosters)) {
res.Boosters.push({
uniqueName: item.typeName,
name: getString(item.name, lang)
});
}
for (const item of Object.values(varzia.primeDualPacks)) {
res.VarziaOffers.push({
uniqueName: item.ItemType,
name: getString(getItemName(item.ItemType) || "", lang)
});
}
for (const [uniqueName, ability] of Object.entries(ExportAbilities)) {
res.Abilities.push({
uniqueName,
name: getString(ability.name || uniqueName, lang)
});
}
response.json(res); response.json(res);
}; };

View File

@ -128,7 +128,7 @@ export const manageQuestsController: RequestHandler = async (req, res) => {
await completeQuest(inventory, questKey.ItemType); await completeQuest(inventory, questKey.ItemType);
} else { } else {
const progress = { const progress = {
c: questManifest.chainStages![currentStage].key ? -1 : 0, c: 0,
i: false, i: false,
m: false, m: false,
b: [] b: []

View File

@ -12,6 +12,7 @@ export const popArchonCrystalUpgradeController: RequestHandler = async (req, res
); );
await inventory.save(); await inventory.save();
res.end(); res.end();
return;
} }
res.status(400).end(); res.status(400).end();
}; };

View File

@ -15,6 +15,7 @@ export const pushArchonCrystalUpgradeController: RequestHandler = async (req, re
} }
await inventory.save(); await inventory.save();
res.end(); res.end();
return;
} }
} }
res.status(400).end(); res.status(400).end();

View File

@ -1,7 +1,7 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountForRequest, isAdministrator, isNameTaken } from "@/src/services/loginService"; import { getAccountForRequest, isAdministrator, isNameTaken } from "@/src/services/loginService";
import { config } from "@/src/services/configService"; import { config } from "@/src/services/configService";
import { saveConfig } from "@/src/services/configWatcherService"; import { saveConfig } from "@/src/services/configWriterService";
export const renameAccountController: RequestHandler = async (req, res) => { export const renameAccountController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req); const account = await getAccountForRequest(req);

View File

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

View File

@ -1,15 +0,0 @@
import { RequestHandler } from "express";
import { updateConfig } from "@/src/services/configWatcherService";
import { getAccountForRequest, isAdministrator } from "@/src/services/loginService";
const updateConfigDataController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req);
if (isAdministrator(account)) {
await updateConfig(String(req.body));
res.end();
} else {
res.status(401).end();
}
};
export { updateConfigDataController };

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