XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFESpaceNinjaServer

A simple server for a small space ninja game

公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/XFESpaceNinjaServer

feat: purchase for old versions (#3249)

It should work fine. Tested on `2013.05.23.16.06` (U8) and `2015.10.15.12.24` (U17.7.1). Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/3249 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>

9335efd7
AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
提交于

代码差异

9 个文件 +407 -31
Modified src/constants/gameToBuildVersion.ts +4 -1
@@ -49,8 +49,11 @@ const gameToBuildVersion = {
49 49 "16.5.5": "2015.05.14.16.29",
50 50 "16.0.2": "2015.03.21.08.17",
51 51 "15.14.1": "2015.02.13.10.41",
52 "15.0.6": "2014.10.27.17.07",
52 53 "15.0.0": "2014.10.24.08.24",
53 54 "14.0.0": "2014.07.21.18.38",
54 "13.0.0": "2014.04.10.17.47"
55 "13.0.0": "2014.04.10.17.47",
56 "10.3.3": "2013.10.11.17.01",
57 "9.1.2": "2013.07.15.20.46"
55 58 } as const;
56 59 export default gameToBuildVersion as Record<keyof typeof gameToBuildVersion, string>;
Modified src/controllers/api/giftingController.ts +24 -2
@@ -15,6 +15,8 @@ import type { IPurchaseParams, IPurchaseResponse } from "../../types/purchaseTyp
15 15 import { PurchaseSource } from "../../types/purchaseTypes.ts";
16 16 import type { RequestHandler } from "express";
17 17 import { ExportBundles, ExportFlavour } from "warframe-public-export-plus";
18 import { logger } from "../../utils/logger.ts";
19 import { getBundle, getPrice } from "../../services/itemDataService.ts";
18 20
19 21 const checkPurchaseParams = (params: IPurchaseParams): boolean => {
20 22 switch (params.Source) {
@@ -29,6 +31,16 @@ const checkPurchaseParams = (params: IPurchaseParams): boolean => {
29 31
30 32 export const giftingController: RequestHandler = async (req, res) => {
31 33 const data = getJSONfromString<IGiftingRequest>(String(req.body));
34 if (!data.PurchaseParams) {
35 const nameParts = String(req.query.productName).split(";");
36 data.PurchaseParams = {
37 Source: PurchaseSource.Market,
38 StoreItem: nameParts[0],
39 Quantity: Number(req.query.quantity),
40 UsePremium: true
41 };
42 if (nameParts[1]) data.PurchaseParams.Durability = Number(nameParts[1]);
43 }
32 44 if (!checkPurchaseParams(data.PurchaseParams)) {
33 45 throw new Error(`unexpected purchase params in gifting request: ${String(req.body)}`);
34 46 }
@@ -76,10 +88,20 @@ export const giftingController: RequestHandler = async (req, res) => {
76 88 if (data.PurchaseParams.Source == PurchaseSource.DailyDeal) {
77 89 await handleDailyDealPurchase(senderInventory, data.PurchaseParams, response);
78 90 } else {
91 if (!data.PurchaseParams.ExpectedPrice) {
92 logger.debug(`client didn't provide ExpectedPrice, attempt to get it from PE+`);
93 data.PurchaseParams.ExpectedPrice = getPrice(
94 data.PurchaseParams.StoreItem,
95 data.PurchaseParams.Quantity,
96 data.PurchaseParams.Durability,
97 data.PurchaseParams.UsePremium,
98 data.buildLabel
99 );
100 }
79 101 updateCurrency(senderInventory, data.PurchaseParams.ExpectedPrice, true, response.InventoryChanges);
80 102 }
81 103 if (data.PurchaseParams.StoreItem in ExportBundles) {
82 const bundle = ExportBundles[data.PurchaseParams.StoreItem];
104 const bundle = getBundle(data.PurchaseParams.StoreItem, data.buildLabel)!;
83 105 if (bundle.giftingBonus) {
84 106 combineInventoryChanges(
85 107 response.InventoryChanges,
@@ -118,7 +140,7 @@ export const giftingController: RequestHandler = async (req, res) => {
118 140 };
119 141
120 142 interface IGiftingRequest {
121 PurchaseParams: IPurchaseParams;
143 PurchaseParams?: IPurchaseParams;
122 144 Message?: string;
123 145 Recipient?: string;
124 146 RecipientId?: IOid;
Modified src/controllers/api/purchaseController.ts +31 -1
@@ -1,13 +1,17 @@
1 1 import type { RequestHandler } from "express";
2 2 import { getAccountForRequest } from "../../services/loginService.ts";
3 import { PurchaseSource } from "../../types/purchaseTypes.ts";
3 4 import type { IPurchaseRequest } from "../../types/purchaseTypes.ts";
4 5 import { handlePurchase } from "../../services/purchaseService.ts";
5 6 import { getInventory } from "../../services/inventoryService.ts";
6 7 import { sendWsBroadcastTo } from "../../services/wsService.ts";
8 import { toStoreItem } from "../../services/itemDataService.ts";
9 import { ExportBundles } from "warframe-public-export-plus";
7 10
8 export const purchaseController: RequestHandler = async (req, res) => {
11 export const purchasePostController: RequestHandler = async (req, res) => {
9 12 const purchaseRequest = JSON.parse(String(req.body)) as IPurchaseRequest;
10 13 const account = await getAccountForRequest(req);
14 if (!purchaseRequest.buildLabel && account.BuildLabel) purchaseRequest.buildLabel = account.BuildLabel;
11 15 if (purchaseRequest.buildLabel && account.BuildLabel && purchaseRequest.buildLabel != account.BuildLabel) {
12 16 throw new Error(
13 17 `account logged into ${account.BuildLabel} but is now attempting a purchase in ${purchaseRequest.buildLabel} ?!`
@@ -21,3 +25,29 @@ export const purchaseController: RequestHandler = async (req, res) => {
21 25 res.json(response);
22 26 sendWsBroadcastTo(accountId, { update_inventory: true });
23 27 };
28
29 export const purchaseGetController: RequestHandler = async (req, res) => {
30 const account = await getAccountForRequest(req);
31 const accountId = account._id.toString();
32 let internalName = String(req.query.productName);
33 if (!(internalName in ExportBundles)) internalName = toStoreItem(internalName);
34 const purchaseRequest: IPurchaseRequest = {
35 PurchaseParams: {
36 Source: PurchaseSource.Market,
37 StoreItem: internalName,
38 Quantity: 1,
39 UsePremium: Boolean(Number(req.query.usePremium))
40 },
41 buildLabel: account.BuildLabel!
42 };
43 if (req.query.durability) purchaseRequest.PurchaseParams.Durability = Number(req.query.durability);
44 const inventory = await getInventory(accountId);
45 const response = await handlePurchase(purchaseRequest, inventory);
46 await inventory.save();
47 if (response.Body) {
48 res.send(response.Body.replace(/\/StoreItems/g, "").replace(/lvl=\d+;/g, ""));
49 } else {
50 res.json(1);
51 }
52 sendWsBroadcastTo(accountId, { update_inventory: true });
53 };
Modified src/routes/api.ts +3 -2
@@ -108,7 +108,7 @@ import { playedParkourTutorialController } from "../controllers/api/playedParkou
108 108 import { playerSkillsController } from "../controllers/api/playerSkillsController.ts";
109 109 import { postGuildAdvertisementController } from "../controllers/api/postGuildAdvertisementController.ts";
110 110 import { projectionManagerController } from "../controllers/api/projectionManagerController.ts";
111 import { purchaseController } from "../controllers/api/purchaseController.ts";
111 import { purchaseGetController, purchasePostController } from "../controllers/api/purchaseController.ts";
112 112 import { questControlController } from "../controllers/api/questControlController.ts";
113 113 import { queueDojoComponentDestructionController } from "../controllers/api/queueDojoComponentDestructionController.ts";
114 114 import { redeemPromoCodeController } from "../controllers/api/redeemPromoCodeController.ts";
@@ -224,6 +224,7 @@ apiRouter.get("/marketRecommendations.php", marketRecommendationsController);
224 224 apiRouter.get("/marketSearchRecommendations.php", marketRecommendationsController);
225 225 apiRouter.get("/modularWeaponSale.php", modularWeaponSaleController);
226 226 apiRouter.get("/playedParkourTutorial.php", playedParkourTutorialController);
227 apiRouter.get("/purchase.php", purchaseGetController); // U8
227 228 apiRouter.get("/questControl.php", questControlController);
228 229 apiRouter.get("/queueDojoComponentDestruction.php", queueDojoComponentDestructionController);
229 230 apiRouter.get("/removeFriend.php", removeFriendGetController);
@@ -329,7 +330,7 @@ apiRouter.post("/placeDecoInComponent.php", placeDecoInComponentController);
329 330 apiRouter.post("/playerSkills.php", playerSkillsController);
330 331 apiRouter.post("/postGuildAdvertisement.php", postGuildAdvertisementController);
331 332 apiRouter.post("/projectionManager.php", projectionManagerController);
332 apiRouter.post("/purchase.php", purchaseController);
333 apiRouter.post("/purchase.php", purchasePostController);
333 334 apiRouter.post("/questControl.php", questControlController); // U17
334 335 apiRouter.post("/redeemPromoCode.php", redeemPromoCodeController);
335 336 apiRouter.post("/releasePet.php", releasePetController);
Modified src/services/inventoryService.ts +2 -2
@@ -72,7 +72,7 @@ import {
72 72 version_compare
73 73 } from "../helpers/inventoryHelpers.ts";
74 74 import { addQuestKey, completeQuest } from "./questService.ts";
75 import { handleBundleAcqusition } from "./purchaseService.ts";
75 import { handleBundleAcquisition } from "./purchaseService.ts";
76 76 import libraryDailyTasks from "../../static/fixed_responses/libraryDailyTasks.json" with { type: "json" };
77 77 import { generateRewardSeed, getRandomElement, getRandomInt, getRandomWeightedReward, SRng } from "./rngService.ts";
78 78 import type { IMessageCreationTemplate } from "./inboxService.ts";
@@ -476,7 +476,7 @@ export const addItem = async (
476 476 ): Promise<IInventoryChanges> => {
477 477 // Bundles are technically StoreItems but a) they don't have a normal counterpart, and b) they are used in non-StoreItem contexts, e.g. email attachments.
478 478 if (typeName in ExportBundles) {
479 return await handleBundleAcqusition(typeName, inventory, quantity);
479 return await handleBundleAcquisition(typeName, inventory, quantity, {}, buildLabel);
480 480 }
481 481
482 482 // Strict typing
Modified src/services/itemDataService.ts +281 -0
@@ -1,5 +1,7 @@
1 1 import type { IKeyChainRequest } from "../types/requestTypes.ts";
2 2 import type {
3 IBoosterPack,
4 IBundle,
3 5 IDefaultUpgrade,
4 6 IInboxMessage,
5 7 IKey,
@@ -25,11 +27,14 @@ import {
25 27 dict_uk,
26 28 dict_zh,
27 29 ExportArcanes,
30 ExportBoosterPacks,
28 31 ExportBoosters,
29 32 ExportBundles,
33 ExportCreditBundles,
30 34 ExportCustoms,
31 35 ExportDojoRecipes,
32 36 ExportDrones,
37 ExportFlavour,
33 38 ExportGear,
34 39 ExportKeys,
35 40 ExportRailjackWeapons,
@@ -374,3 +379,279 @@ export const getProductCategory = (uniqueName: string): string => {
374 379 }
375 380 throw new Error(`don't know product category of ${uniqueName}`);
376 381 };
382
383 export const getBundle = (uniqueName: string, buildLabel: string = ""): IBundle | undefined => {
384 if (buildLabel) {
385 if (
386 uniqueName == "/Lotus/Types/StoreItems/Packages/StalkerPack" &&
387 version_compare(buildLabel, "2024.06.12.18.42") < 0 // < 36.0.0
388 ) {
389 return {
390 name: "/Lotus/Language/Items/StalkerPackName",
391 description: "/Lotus/Language/Items/StalkerPackDesc",
392 icon: "/Lotus/Interface/Icons/StoreIcons/MarketBundles/Weapons/StalkerPack.png",
393 components: [
394 { typeName: "/Lotus/StoreItems/Weapons/Tenno/Bows/StalkerBow", purchaseQuantity: 1 },
395 { typeName: "/Lotus/StoreItems/Weapons/Tenno/ThrowingWeapons/StalkerKunai", purchaseQuantity: 1 },
396 {
397 typeName: "/Lotus/StoreItems/Weapons/Tenno/Melee/Scythe/StalkerScytheWeapon",
398 purchaseQuantity: 1
399 },
400 {
401 typeName: "/Lotus/StoreItems/Types/StoreItems/SuitCustomizations/NinjaColourPickerItem",
402 purchaseQuantity: 1
403 }
404 ],
405 packageDiscount: 0.059
406 };
407 }
408 }
409
410 return ExportBundles[uniqueName];
411 };
412
413 export const getBoosterPack = (uniqueName: string, buildLabel: string = ""): IBoosterPack | undefined => {
414 if (
415 version_compare(buildLabel, gameToBuildVersion["18.16.0"]) < 0 &&
416 uniqueName == "/Lotus/Types/BoosterPacks/RandomKey"
417 ) {
418 const boosterPack: IBoosterPack = {
419 name: "/Lotus/Language/Items/RandomKey",
420 description: "/Lotus/Language/Items/RandomKeyDesc",
421 icon: "/Lotus/Interface/Icons/Store/OrokinKey.png",
422 components: [
423 { Item: "/Lotus/Types/Keys/OrokinKeyA", Rarity: "COMMON", Amount: 1 },
424 { Item: "/Lotus/Types/Keys/OrokinKeyB", Rarity: "COMMON", Amount: 1 },
425 { Item: "/Lotus/Types/Keys/OrokinKeyC", Rarity: "UNCOMMON", Amount: 1 },
426 { Item: "/Lotus/Types/Keys/OrokinKeyD", Rarity: "UNCOMMON", Amount: 1 },
427 { Item: "/Lotus/Types/Keys/OrokinKeyE", Rarity: "RARE", Amount: 1 }
428 ],
429 rarityWeightsPerRoll: [
430 { COMMON: 1, UNCOMMON: 0.050000001, RARE: 0.0099999998, LEGENDARY: 0 },
431 { COMMON: 1, UNCOMMON: 0.25, RARE: 0.050000001, LEGENDARY: 0 },
432 { COMMON: 1, UNCOMMON: 0.25, RARE: 0.050000001, LEGENDARY: 0 },
433 { COMMON: 1, UNCOMMON: 0.25, RARE: 0.050000001, LEGENDARY: 0 },
434 { COMMON: 1, UNCOMMON: 0.25, RARE: 0.1, LEGENDARY: 0 }
435 ],
436 canGiveDuplicates: true,
437 platinumCost: 75
438 };
439 if (buildLabel) {
440 if (version_compare(buildLabel, "2013.06.07.23.44") >= 0) {
441 boosterPack.rarityWeightsPerRoll[4] = { COMMON: 0, UNCOMMON: 0, RARE: 1, LEGENDARY: 0 };
442 }
443 if (version_compare(buildLabel, gameToBuildVersion["9.1.2"]) >= 0) {
444 boosterPack.components.push(
445 { Item: "/Lotus/Types/Keys/OrokinCaptureKeyA", Rarity: "COMMON", Amount: 1 },
446 { Item: "/Lotus/Types/Keys/OrokinCaptureKeyB", Rarity: "COMMON", Amount: 1 },
447 { Item: "/Lotus/Types/Keys/OrokinCaptureKeyC", Rarity: "RARE", Amount: 1 },
448 { Item: "/Lotus/Types/Keys/OrokinMobileDefenseKeyA", Rarity: "COMMON", Amount: 1 },
449 { Item: "/Lotus/Types/Keys/OrokinMobileDefenseKeyB", Rarity: "UNCOMMON", Amount: 1 },
450 { Item: "/Lotus/Types/Keys/OrokinMobileDefenseKeyC", Rarity: "RARE", Amount: 1 },
451 { Item: "/Lotus/Types/Keys/OrokinDefenseKeyA", Rarity: "COMMON", Amount: 1 },
452 { Item: "/Lotus/Types/Keys/OrokinDefenseKeyB", Rarity: "UNCOMMON", Amount: 1 },
453 { Item: "/Lotus/Types/Keys/OrokinDefenseKeyC", Rarity: "RARE", Amount: 1 }
454 );
455 }
456 if (version_compare(buildLabel, gameToBuildVersion["10.3.3"]) >= 0) {
457 boosterPack.components.push({
458 Item: "/Lotus/Types/Keys/OrokinTowerSurvivalT3Key",
459 Rarity: "UNCOMMON",
460 Amount: 1
461 });
462 }
463 if (version_compare(buildLabel, gameToBuildVersion["14.0.0"]) >= 0) {
464 boosterPack.components.find(c => c.Item === "/Lotus/Types/Keys/OrokinTowerSurvivalT3Key")!.Rarity =
465 "RARE";
466 boosterPack.components.push(
467 { Item: "/Lotus/Types/Keys/OrokinTowerKeys/OrokinTowerCaptureTier4Key", Rarity: "RARE", Amount: 1 },
468 { Item: "/Lotus/Types/Keys/OrokinTowerKeys/OrokinTowerDefenseTier4Key", Rarity: "RARE", Amount: 1 },
469 {
470 Item: "/Lotus/Types/Keys/OrokinTowerKeys/OrokinTowerExterminateTier4Key",
471 Rarity: "RARE",
472 Amount: 1
473 },
474 {
475 Item: "/Lotus/Types/Keys/OrokinTowerKeys/OrokinTowerInterceptionTier4Key",
476 Rarity: "RARE",
477 Amount: 1
478 },
479 {
480 Item: "/Lotus/Types/Keys/OrokinTowerKeys/OrokinTowerMobileDefenseTier4Key",
481 Rarity: "RARE",
482 Amount: 1
483 },
484 { Item: "/Lotus/Types/Keys/OrokinTowerKeys/OrokinTowerSurvivalTier4Key", Rarity: "RARE", Amount: 1 }
485 );
486 }
487 if (version_compare(buildLabel, gameToBuildVersion["15.0.6"]) >= 0) {
488 boosterPack.components.push(
489 {
490 Item: "/Lotus/Types/Keys/OrokinTowerKeys/OrokinTowerSabotageTier1Key",
491 Rarity: "COMMON",
492 Amount: 1
493 },
494 {
495 Item: "/Lotus/Types/Keys/OrokinTowerKeys/OrokinTowerSabotageTier2Key",
496 Rarity: "UNCOMMON",
497 Amount: 1
498 },
499 {
500 Item: "/Lotus/Types/Keys/OrokinTowerKeys/OrokinTowerSabotageTier3Key",
501 Rarity: "RARE",
502 Amount: 1
503 },
504 { Item: "/Lotus/Types/Keys/OrokinTowerKeys/OrokinTowerSabotageTier4Key", Rarity: "RARE", Amount: 1 }
505 );
506 }
507 }
508 return boosterPack;
509 }
510 if (version_compare(buildLabel, gameToBuildVersion["18.18.0"]) < 0) {
511 if (uniqueName == "/Lotus/Types/BoosterPacks/CommonFusionPack") {
512 return {
513 name: "/Lotus/Language/Items/CommonFusionPack",
514 description: "/Lotus/Language/Items/CommonFusionPackDesc",
515 icon: "/Lotus/Interface/Icons/Store/FusionCorePackBronze.png",
516 components: [
517 { Item: "/Lotus/Upgrades/Mods/Fusers/RareModFuser", Rarity: "RARE", Amount: 1 },
518 { Item: "/Lotus/Upgrades/Mods/Fusers/UncommonModFuser", Rarity: "UNCOMMON", Amount: 1 },
519 { Item: "/Lotus/Upgrades/Mods/Fusers/CommonModFuser", Rarity: "COMMON", Amount: 1 }
520 ],
521 rarityWeightsPerRoll: [
522 { COMMON: 0.60000002, UNCOMMON: 0.40000001, RARE: 0, LEGENDARY: 0 },
523 { COMMON: 0.60000002, UNCOMMON: 0.40000001, RARE: 0, LEGENDARY: 0 },
524 { COMMON: 0.5, UNCOMMON: 0.30000001, RARE: 0.2, LEGENDARY: 0 }
525 ],
526 canGiveDuplicates: true,
527 platinumCost: 55
528 };
529 }
530 if (uniqueName == "/Lotus/Types/BoosterPacks/PremiumUncommonFusionPack") {
531 return {
532 name: "/Lotus/Language/Items/PremiumUncommonFusionPack",
533 description: "/Lotus/Language/Items/PremiumUncommonFusionPackDesc",
534 icon: "/Lotus/Interface/Icons/Store/FusionCorePackSilver.png",
535 components: [
536 { Item: "/Lotus/Upgrades/Mods/Fusers/RareModFuser", Rarity: "RARE", Amount: 1 },
537 { Item: "/Lotus/Upgrades/Mods/Fusers/UncommonModFuser", Rarity: "UNCOMMON", Amount: 1 },
538 { Item: "/Lotus/Upgrades/Mods/Fusers/CommonModFuser", Rarity: "COMMON", Amount: 1 }
539 ],
540 rarityWeightsPerRoll: [
541 { COMMON: 0, UNCOMMON: 1, RARE: 0, LEGENDARY: 0 },
542 { COMMON: 0.5, UNCOMMON: 0.30000001, RARE: 0.2, LEGENDARY: 0 },
543 { COMMON: 0.5, UNCOMMON: 0.30000001, RARE: 0.2, LEGENDARY: 0 }
544 ],
545 canGiveDuplicates: true,
546 platinumCost: 70
547 };
548 }
549 if (uniqueName == "/Lotus/Types/BoosterPacks/PremiumRareFusionPack") {
550 return {
551 name: "/Lotus/Language/Items/PremiumRareFusionPack",
552 description: "/Lotus/Language/Items/PremiumRareFusionPackDesc",
553 icon: "/Lotus/Interface/Icons/Store/FusionCorePackGold.png",
554 components: [
555 { Item: "/Lotus/Upgrades/Mods/Fusers/CommonModFuser", Rarity: "COMMON", Amount: 1 },
556 { Item: "/Lotus/Upgrades/Mods/Fusers/UncommonModFuser", Rarity: "UNCOMMON", Amount: 1 },
557 { Item: "/Lotus/Upgrades/Mods/Fusers/RareModFuser", Rarity: "RARE", Amount: 1 }
558 ],
559 rarityWeightsPerRoll: [
560 { COMMON: 0.5, UNCOMMON: 0.30000001, RARE: 0.2, LEGENDARY: 0 },
561 { COMMON: 0.5, UNCOMMON: 0.30000001, RARE: 0.2, LEGENDARY: 0 },
562 { COMMON: 0, UNCOMMON: 0, RARE: 1, LEGENDARY: 0 }
563 ],
564 canGiveDuplicates: true,
565 platinumCost: 80
566 };
567 }
568 }
569
570 return ExportBoosterPacks[uniqueName];
571 };
572
573 export const getPrice = (
574 storeItemName: string,
575 quantity: number = 1,
576 durability: number = 0,
577 usePremium: boolean,
578 buildLabel: string
579 ): number => {
580 let price: number | undefined;
581 const isBundle = storeItemName in ExportBundles;
582 const isBooster = storeItemName in ExportBoosters;
583 if (isBooster) {
584 price = 40 * (durability + 1);
585 } else if (isBundle) {
586 const bundle = getBundle(storeItemName, buildLabel)!;
587 if (usePremium && bundle.platinumCost) {
588 price = ExportBundles[storeItemName].platinumCost;
589 } else if (!usePremium && bundle.creditsCost) {
590 price = ExportBundles[storeItemName].creditsCost;
591 } else {
592 let sum = 0;
593 for (const component of bundle.components) {
594 sum += getPrice(
595 component.typeName,
596 component.purchaseQuantity,
597 [3, 7, 30, 90].indexOf(component.durabilityDays ?? 3),
598 usePremium,
599 buildLabel
600 );
601 }
602 const discount = typeof bundle.packageDiscount === "number" ? bundle.packageDiscount : 0.25;
603 price = Math.round(sum * (1 - discount));
604 }
605 } else {
606 const internalName = fromStoreItem(storeItemName);
607 const boosterPack = getBoosterPack(fromStoreItem(storeItemName), buildLabel);
608 const isBoosterPack = boosterPack !== undefined;
609 if (isBoosterPack) {
610 if (usePremium) price = boosterPack.platinumCost;
611 } else {
612 const categories = [
613 ExportBundles,
614 ExportCreditBundles,
615 ExportCustoms,
616 ExportFlavour,
617 ExportGear,
618 ExportRecipes,
619 ExportResources,
620 ExportSentinels,
621 ExportWarframes,
622 ExportWeapons
623 ];
624 const category = categories.find(c => internalName in c);
625 if (category) {
626 const item = category[internalName];
627 if (usePremium && "platinumCost" in item) {
628 price = item.platinumCost;
629 } else if (!usePremium && "creditsCost" in item) {
630 price = item.creditsCost;
631 }
632 }
633
634 if (usePremium) {
635 if (version_compare(buildLabel, gameToBuildVersion["18.16.0"]) < 0) {
636 if (internalName == "/Lotus/Powersuits/Mag/Mag") {
637 price = ExportWarframes["/Lotus/Powersuits/Loki/Loki"].platinumCost;
638 } else if (internalName == "/Lotus/Powersuits/Loki/Loki") {
639 price = ExportWarframes["/Lotus/Powersuits/Mag/Mag"].platinumCost;
640 }
641 }
642 if (version_compare(buildLabel, gameToBuildVersion["18.0.2"]) < 0) {
643 if (internalName == "/Lotus/Upgrades/Skins/Dragon/DragonAltHelmet") price = 40;
644 }
645 } else {
646 // I'm not sure when they stopped selling it
647 if (storeItemName == "/Lotus/StoreItems/Types/Restoratives/Cipher") price = 250;
648 }
649 }
650 }
651
652 if (price == undefined) {
653 throw new Error(`no price found for ${storeItemName}`);
654 }
655
656 return price * quantity;
657 };
Modified src/services/purchaseService.ts +54 -17
@@ -21,7 +21,6 @@ import { PurchaseSource } from "../types/purchaseTypes.ts";
21 21 import { logger } from "../utils/logger.ts";
22 22 import { getWorldState } from "./worldStateService.ts";
23 23 import {
24 ExportBoosterPacks,
25 24 ExportBoosters,
26 25 ExportBundles,
27 26 ExportCreditBundles,
@@ -31,7 +30,7 @@ import {
31 30 ExportVendors
32 31 } from "warframe-public-export-plus";
33 32 import type { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel.ts";
34 import { fromStoreItem, toStoreItem } from "./itemDataService.ts";
33 import { fromStoreItem, getBoosterPack, getBundle, getPrice, toStoreItem } from "./itemDataService.ts";
35 34 import { DailyDeal } from "../models/worldStateModel.ts";
36 35 import { fromMongoDate, toMongoDate } from "../helpers/inventoryHelpers.ts";
37 36 import { Guild } from "../models/guildModel.ts";
@@ -200,10 +199,22 @@ export const handlePurchase = async (
200 199 undefined,
201 200 false,
202 201 purchaseRequest.PurchaseParams.UsePremium,
203 seed
202 seed,
203 purchaseRequest.buildLabel
204 204 );
205 205 combineInventoryChanges(purchaseResponse.InventoryChanges, prePurchaseInventoryChanges);
206 206
207 if (!purchaseRequest.PurchaseParams.ExpectedPrice) {
208 logger.debug(`client didn't provide ExpectedPrice, attempt to get it from PE+`);
209 purchaseRequest.PurchaseParams.ExpectedPrice = getPrice(
210 purchaseRequest.PurchaseParams.StoreItem,
211 purchaseRequest.PurchaseParams.Quantity,
212 purchaseRequest.PurchaseParams.Durability,
213 purchaseRequest.PurchaseParams.UsePremium,
214 purchaseRequest.buildLabel
215 );
216 }
217
207 218 updateCurrency(
208 219 inventory,
209 220 purchaseRequest.PurchaseParams.ExpectedPrice,
@@ -402,13 +413,14 @@ export const handleDailyDealPurchase = async (
402 413 }
403 414 };
404 415
405 export const handleBundleAcqusition = async (
416 export const handleBundleAcquisition = async (
406 417 storeItemName: string,
407 418 inventory: TInventoryDatabaseDocument,
408 419 quantity: number = 1,
409 inventoryChanges: IInventoryChanges = {}
420 inventoryChanges: IInventoryChanges = {},
421 buildLabel?: string
410 422 ): Promise<IInventoryChanges> => {
411 const bundle = ExportBundles[storeItemName];
423 const bundle = getBundle(storeItemName, buildLabel)!;
412 424 logger.debug("acquiring bundle", bundle);
413 425 for (const component of bundle.components) {
414 426 combineInventoryChanges(
@@ -419,7 +431,10 @@ export const handleBundleAcqusition = async (
419 431 inventory,
420 432 component.purchaseQuantity * quantity,
421 433 component.durabilityDays,
422 true
434 true,
435 true,
436 undefined,
437 buildLabel
423 438 )
424 439 ).InventoryChanges
425 440 );
@@ -434,14 +449,21 @@ export const handleStoreItemAcquisition = async (
434 449 durabilityDays: number = 3,
435 450 ignorePurchaseQuantity: boolean = false,
436 451 premiumPurchase: boolean = true,
437 seed?: bigint
452 seed?: bigint,
453 buildLabel?: string
438 454 ): Promise<IPurchaseResponse> => {
439 455 let purchaseResponse = {
440 456 InventoryChanges: {}
441 457 };
442 logger.debug(`handling acquision of ${storeItemName}`);
458 logger.debug(`handling acquisition of ${storeItemName}`);
443 459 if (storeItemName in ExportBundles) {
444 await handleBundleAcqusition(storeItemName, inventory, quantity, purchaseResponse.InventoryChanges);
460 await handleBundleAcquisition(
461 storeItemName,
462 inventory,
463 quantity,
464 purchaseResponse.InventoryChanges,
465 buildLabel
466 );
445 467 } else {
446 468 const storeCategory = getStoreItemCategory(storeItemName);
447 469 const internalName = fromStoreItem(storeItemName);
@@ -477,7 +499,8 @@ export const handleStoreItemAcquisition = async (
477 499 quantity,
478 500 ignorePurchaseQuantity,
479 501 premiumPurchase,
480 seed
502 seed,
503 buildLabel
481 504 );
482 505 break;
483 506 case "Boosters":
@@ -544,15 +567,17 @@ const handleSlotPurchase = (
544 567 const handleBoosterPackPurchase = async (
545 568 typeName: string,
546 569 inventory: TInventoryDatabaseDocument,
547 quantity: number
570 quantity: number,
571 buildLabel?: string
548 572 ): Promise<IPurchaseResponse> => {
549 const pack = ExportBoosterPacks[typeName];
573 const pack = getBoosterPack(typeName, buildLabel);
550 574 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
551 575 if (!pack) {
552 576 throw new Error(`unknown booster pack: ${typeName}`);
553 577 }
554 578 const purchaseResponse: IPurchaseResponse = {
555 579 BoosterPackItems: "",
580 Body: "",
556 581 InventoryChanges: {}
557 582 };
558 583 if (quantity < 1) {
@@ -622,15 +647,26 @@ const handleBoosterPackPurchase = async (
622 647 if (!pack.canGiveDuplicates) {
623 648 disallowedItems.add(result.Item);
624 649 }
625 purchaseResponse.BoosterPackItems += toStoreItem(result.Item) + ',{"lvl":0};';
650 const fingerprint = { lvl: 0 };
651 if (result.Item.startsWith("/Lotus/Upgrades/Mods/Fusers/")) {
652 if (result.Item.endsWith("RareModFuser")) {
653 fingerprint.lvl = Math.floor(Math.random() * 4) + 2;
654 } else if (result.Item.endsWith("CommonModFuser") || result.Item.endsWith("UncommonModFuser")) {
655 fingerprint.lvl = Math.floor(Math.random() * 3) + 1;
656 }
657 }
658 const stringifiedFingerprint = JSON.stringify(fingerprint);
659 purchaseResponse.BoosterPackItems += toStoreItem(result.Item) + `,${stringifiedFingerprint};`;
626 660 combineInventoryChanges(
627 661 purchaseResponse.InventoryChanges,
628 await addItem(inventory, result.Item, result.Amount)
662 await addItem(inventory, result.Item, result.Amount, false, undefined, stringifiedFingerprint)
629 663 );
630 664 ++roll;
631 665 }
632 666 }
633 667 }
668 if (purchaseResponse.BoosterPackItems)
669 purchaseResponse.Body = purchaseResponse.BoosterPackItems.replace(/[{}"]/g, "").replace(/:/g, "=");
634 670 return purchaseResponse;
635 671 };
636 672
@@ -657,7 +693,8 @@ const handleTypesPurchase = async (
657 693 quantity: number,
658 694 ignorePurchaseQuantity: boolean,
659 695 premiumPurchase: boolean = true,
660 seed?: bigint
696 seed?: bigint,
697 buildLabel?: string
661 698 ): Promise<IPurchaseResponse> => {
662 699 const typeCategory = getStoreItemTypesCategory(typesName);
663 700 logger.debug(`type category ${typeCategory}`);
@@ -667,7 +704,7 @@ const handleTypesPurchase = async (
667 704 InventoryChanges: await addItem(inventory, typesName, quantity, premiumPurchase, seed, undefined, true)
668 705 };
669 706 case "BoosterPacks":
670 return handleBoosterPackPurchase(typesName, inventory, quantity);
707 return handleBoosterPackPurchase(typesName, inventory, quantity, buildLabel);
671 708 case "SlotItems":
672 709 return handleSlotPurchase(typesName, inventory, quantity, ignorePurchaseQuantity);
673 710 case "CreditBundles":
Modified src/services/questService.ts +2 -2
@@ -16,7 +16,7 @@ import { logger } from "../utils/logger.ts";
16 16 import { ExportKeys, ExportRecipes } from "warframe-public-export-plus";
17 17 import { addFixedLevelRewards } from "./missionInventoryUpdateService.ts";
18 18 import { fromOid } from "../helpers/inventoryHelpers.ts";
19 import { handleBundleAcqusition } from "./purchaseService.ts";
19 import { handleBundleAcquisition } from "./purchaseService.ts";
20 20 import type { IInventoryChanges } from "../types/purchaseTypes.ts";
21 21 import questCompletionItems from "../../static/fixed_responses/questCompletionRewards.json" with { type: "json" };
22 22 import type { ITypeCount } from "../types/commonTypes.ts";
@@ -304,7 +304,7 @@ const handleQuestCompletion = async (
304 304 }
305 305 if (!syndicate.Initiated) {
306 306 syndicate.Initiated = true;
307 await handleBundleAcqusition("/Lotus/Types/StoreItems/Packages/SanctuaryInitiationKit", inventory);
307 await handleBundleAcquisition("/Lotus/Types/StoreItems/Packages/SanctuaryInitiationKit", inventory);
308 308 }
309 309 } else if (questKey == "/Lotus/Types/Keys/NewWarQuest/NewWarQuestKeyChain" && !isRerun) {
310 310 setupKahlSyndicate(inventory);
Modified src/types/purchaseTypes.ts +6 -4
@@ -53,12 +53,13 @@ export interface IPurchaseParams {
53 53 Source: PurchaseSource;
54 54 SourceId?: string; // VoidTrader, Vendor, PrimeVaultTrader
55 55 StoreItem: string;
56 StorePage: string;
57 SearchTerm: string;
58 CurrentLocation: string;
56 StorePage?: string;
57 SearchTerm?: string;
58 CurrentLocation?: string;
59 59 Quantity: number;
60 60 UsePremium: boolean;
61 ExpectedPrice: number;
61 ExpectedPrice?: number;
62 Durability?: number;
62 63 SyndicateTag?: string; // SyndicateFavor
63 64 UseFreeFavor?: boolean; // SyndicateFavor
64 65 ExtraPurchaseInfoJson?: string; // Vendor
@@ -120,6 +121,7 @@ export interface IPurchaseResponse {
120 121 Standing?: IAffiliationMods[];
121 122 FreeFavorsUsed?: IAffiliationMods[];
122 123 BoosterPackItems?: string;
124 Body?: string;
123 125 DailyDealUsed?: string;
124 126 }
125 127