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

SpaceNinjaServer

A simple server for a small space ninja game

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

XFEstudio/SpaceNinjaServer

feat: trading (#3160)

Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/3160 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>

35af72ec
Sainan <63328889+Sainan@users.noreply.github.com>
提交于

代码差异

7 个文件 +338 -43
Modified package-lock.json +4 -4
@@ -17,7 +17,7 @@
17 17 "morgan": "^1.10.0",
18 18 "ncp": "^2.0.0",
19 19 "undici": "^7.10.0",
20 "warframe-public-export-plus": "^0.5.96",
20 "warframe-public-export-plus": "^0.5.97",
21 21 "warframe-riven-info": "^0.1.2",
22 22 "winston": "^3.17.0",
23 23 "winston-daily-rotate-file": "^5.0.0",
@@ -5421,9 +5421,9 @@
5421 5421 }
5422 5422 },
5423 5423 "node_modules/warframe-public-export-plus": {
5424 "version": "0.5.96",
5425 "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.96.tgz",
5426 "integrity": "sha512-ipOJEXzmrI+AryyrpHfd0ZzTV7arVQLm+ih9zi1FxlO96sDtNcZevqL32Q3OZvOk70aDywK1KeGc5gWshCcPxw=="
5424 "version": "0.5.97",
5425 "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.97.tgz",
5426 "integrity": "sha512-Xe07uNXT7Pci0yDIdREHuJBx+uTspf/qmPNh0oprjSlyFlib4HjrKm2iSNnw9q4NUqxzv42SwumMD8hCsAOnng=="
5427 5427 },
5428 5428 "node_modules/warframe-riven-info": {
5429 5429 "version": "0.1.2",
Modified package.json +1 -1
@@ -34,7 +34,7 @@
34 34 "morgan": "^1.10.0",
35 35 "ncp": "^2.0.0",
36 36 "undici": "^7.10.0",
37 "warframe-public-export-plus": "^0.5.96",
37 "warframe-public-export-plus": "^0.5.97",
38 38 "warframe-riven-info": "^0.1.2",
39 39 "winston": "^3.17.0",
40 40 "winston-daily-rotate-file": "^5.0.0",
Modified src/controllers/api/tradingController.ts +317 -6
@@ -1,15 +1,125 @@
1 1 import { getGuildForRequestEx, hasAccessToDojo, hasGuildPermission } from "../../services/guildService.ts";
2 import { getInventory } from "../../services/inventoryService.ts";
3 import { getAccountIdForRequest } from "../../services/loginService.ts";
2 import {
3 addFusionTreasures,
4 addMiscItem,
5 addMods,
6 addRecipes,
7 getInventory,
8 updateCurrency
9 } from "../../services/inventoryService.ts";
10 import { getAccountForRequest } from "../../services/loginService.ts";
4 11 import { GuildPermission } from "../../types/guildTypes.ts";
5 12 import type { RequestHandler } from "express";
13 import type { IPendingTrade, ITradeOffer } from "../../types/inventoryTypes/inventoryTypes.ts";
14 import { getJSONfromString } from "../../helpers/stringHelpers.ts";
15 import { logger } from "../../utils/logger.ts";
16 import { version_compare } from "../../helpers/inventoryHelpers.ts";
17 import gameToBuildVersion from "../../constants/gameToBuildVersion.ts";
18 import type { TInventoryDatabaseDocument } from "../../models/inventoryModels/inventoryModel.ts";
19 import { importUpgrade } from "../../services/importService.ts";
20 import { Guild } from "../../models/guildModel.ts";
21 import { ExportArcanes, ExportResources, ExportUpgrades, type TRarity } from "warframe-public-export-plus";
6 22
7 23 export const tradingController: RequestHandler = async (req, res) => {
8 const accountId = await getAccountIdForRequest(req);
9 const inventory = await getInventory(accountId, "GuildId LevelKeys");
10 const guild = await getGuildForRequestEx(req, inventory);
24 const account = await getAccountForRequest(req);
25 const accountId = account._id.toString();
11 26 const op = req.query.op as string;
12 if (op == "5") {
27 if (op == "0" || op == "1") {
28 const buddyId = req.query.buddyId as string;
29 const tradeId = getTradeId(accountId, buddyId);
30 const offer = getJSONfromString<ITradeOffer>(String(req.body));
31 pendingTrades[tradeId] ??= {
32 trader: accountId,
33 tradee: buddyId,
34 traderOffer: { _SlotOrderInfo: ["", "", "", "", "", ""] },
35 tradeeOffer: { _SlotOrderInfo: ["", "", "", "", "", ""] },
36 traderReady: false,
37 tradeeReady: false,
38 traderAccepted: false,
39 tradeeAccepted: false,
40 revision: -1,
41 clanTax: (req.query.guildId ? await Guild.findById(req.query.guildId as string) : undefined)?.TradeTax ?? 10 // When trading in Maroo's Bazaar, a 10% "Hub Tax" applies.
42 };
43 const us = accountId == pendingTrades[tradeId].trader ? "trader" : "tradee";
44 logger.debug(`${us} offer:`, offer);
45 {
46 const tax = calcuateTax(offer);
47 const clanTaxRate = pendingTrades[tradeId].clanTax ? 1 / pendingTrades[tradeId].clanTax : 0;
48 const clanTax = clanTaxRate * tax;
49 logger.debug(`calculated tax: ${tax} + ${clanTax}`);
50 }
51 pendingTrades[tradeId][`${us}Offer`] = offer;
52 pendingTrades[tradeId].traderReady = false;
53 pendingTrades[tradeId].tradeeReady = false;
54 pendingTrades[tradeId].traderAccepted = false;
55 pendingTrades[tradeId].tradeeAccepted = false;
56 pendingTrades[tradeId].revision += 1;
57 res.json({
58 PendingTrades: [exportTrade(pendingTrades[tradeId], us)]
59 });
60 } else if (op == "2") {
61 const buddyId = req.query.buddyId as string;
62 const tradeId = getTradeId(accountId, buddyId);
63 if (parseInt(req.query.revision as string) != pendingTrades[tradeId].revision) {
64 throw new Error(`can't accept trade because it was unexpectedly revised`);
65 }
66 const us = accountId == pendingTrades[tradeId].trader ? "trader" : "tradee";
67 pendingTrades[tradeId][`${us}Accepted`] = true;
68 const them = us == "trader" ? "tradee" : "trader";
69 if (pendingTrades[tradeId][`${them}Accepted`]) {
70 const [traderInventory, tradeeInventory] = await Promise.all([
71 getInventory(pendingTrades[tradeId].trader),
72 getInventory(pendingTrades[tradeId].tradee)
73 ]);
74
75 applyOfferToInventory(traderInventory, pendingTrades[tradeId].traderOffer, -1); // Traders gives up their items
76 applyOfferToInventory(traderInventory, pendingTrades[tradeId].tradeeOffer, +1); // to get what tradee offered
77 // and pays tax for it
78 await chargeTax(
79 traderInventory,
80 pendingTrades[tradeId].tradeeOffer,
81 pendingTrades[tradeId].clanTax,
82 req.query.guildId as string | undefined
83 );
84 applyOfferToInventory(tradeeInventory, pendingTrades[tradeId].tradeeOffer, -1); // Conversely, tradee gives up their items
85 applyOfferToInventory(tradeeInventory, pendingTrades[tradeId].traderOffer, +1); // to get what trader offered
86 // and pays tax for it
87 await chargeTax(
88 tradeeInventory,
89 pendingTrades[tradeId].traderOffer,
90 pendingTrades[tradeId].clanTax,
91 req.query.guildId as string | undefined
92 );
93
94 traderInventory.TradesRemaining -= 1;
95 tradeeInventory.TradesRemaining -= 1;
96
97 await Promise.all([traderInventory.save(), tradeeInventory.save()]);
98 delete pendingTrades[tradeId];
99 res.send(
100 account.BuildLabel && version_compare(account.BuildLabel, gameToBuildVersion["40.0.0"]) < 0 ? "9" : "12"
101 ).end();
102 } else {
103 res.send(
104 account.BuildLabel && version_compare(account.BuildLabel, gameToBuildVersion["40.0.0"]) < 0 ? "8" : "11"
105 ).end();
106 }
107 } else if (op == "3") {
108 const buddyId = req.query.buddyId as string;
109 const tradeId = getTradeId(accountId, buddyId);
110 delete pendingTrades[tradeId];
111 res.json({
112 pendingTrades: []
113 });
114 } else if (op == "4") {
115 const tradeId = getTradeId(accountId, req.query.buddyId as string);
116 const us = accountId == pendingTrades[tradeId].trader ? "trader" : "tradee";
117 res.json({
118 PendingTrades: [exportTrade(pendingTrades[tradeId], us)]
119 });
120 } else if (op == "5") {
121 const inventory = await getInventory(accountId, "GuildId LevelKeys");
122 const guild = await getGuildForRequestEx(req, inventory);
13 123 if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Treasurer))) {
14 124 res.status(400).send("-1").end();
15 125 return;
@@ -17,7 +127,208 @@ export const tradingController: RequestHandler = async (req, res) => {
17 127 guild.TradeTax = parseInt(req.query.tax as string);
18 128 await guild.save();
19 129 res.send(guild.TradeTax).end();
130 } else if (op == "6") {
131 const buddyId = req.query.buddyId as string;
132 const tradeId = getTradeId(accountId, buddyId);
133 const us = accountId == pendingTrades[tradeId].trader ? "trader" : "tradee";
134 if (parseInt(req.query.revision as string) == pendingTrades[tradeId].revision) {
135 pendingTrades[tradeId][`${us}Ready`] = true;
136 }
137 res.json({
138 PendingTrades: [exportTrade(pendingTrades[tradeId], us)]
139 });
140 } else if (op == "7") {
141 const buddyId = req.query.buddyId as string;
142 const tradeId = getTradeId(accountId, buddyId);
143 const us = accountId == pendingTrades[tradeId].trader ? "trader" : "tradee";
144 pendingTrades[tradeId][`${us}Ready`] = false;
145 pendingTrades[tradeId].tradeeAccepted = false;
146 pendingTrades[tradeId].traderAccepted = false;
147 res.json({
148 PendingTrades: [exportTrade(pendingTrades[tradeId], us)]
149 });
20 150 } else {
151 if (req.body) {
152 logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
153 }
21 154 throw new Error(`unknown trading op: ${op}`);
22 155 }
23 156 };
157
158 interface IPendingTradeMem {
159 trader: string;
160 tradee: string;
161 traderOffer: ITradeOffer;
162 tradeeOffer: ITradeOffer;
163 traderReady: boolean;
164 tradeeReady: boolean;
165 traderAccepted: boolean;
166 tradeeAccepted: boolean;
167 revision: number;
168 clanTax: number;
169 }
170
171 const pendingTrades: Record<string, IPendingTradeMem> = {};
172
173 const getTradeId = (accountId: string, buddyId: string): string => {
174 return accountId.localeCompare(buddyId) < 0 ? accountId + buddyId : buddyId + accountId;
175 };
176
177 const exportTrade = (mem: IPendingTradeMem, us: "trader" | "tradee"): IPendingTrade => {
178 const them = us == "trader" ? "tradee" : "trader";
179 return {
180 ItemId: { $oid: mem[them] },
181 Giving: mem[`${us}Offer`],
182 Getting: mem[`${them}Offer`],
183 SelfReady: mem[`${us}Ready`],
184 BuddyReady: mem[`${them}Ready`],
185 State: mem[`${us}Accepted`] ? (mem[`${them}Accepted`] ? 4 : 3) : 2,
186 Revision: mem.revision,
187 ClanTax: mem.clanTax
188 };
189 };
190
191 const applyOfferToInventory = (inventory: TInventoryDatabaseDocument, offer: ITradeOffer, factor: 1 | -1): void => {
192 if (offer.RandomUpgrades) {
193 for (const item of offer.RandomUpgrades) {
194 if (factor == -1) {
195 inventory.Upgrades.pull({ _id: item.ItemId.$oid });
196 } else {
197 inventory.Upgrades.push(importUpgrade(item));
198 }
199 }
200 }
201 if (offer.Upgrades) {
202 for (const item of offer.Upgrades) {
203 if (factor == -1) {
204 inventory.Upgrades.pull({ _id: item.ItemId.$oid });
205 } else {
206 inventory.Upgrades.push(importUpgrade(item));
207 }
208 }
209 }
210 if (offer.RawUpgrades) {
211 if (factor == 1) {
212 addMods(inventory, offer.RawUpgrades);
213 } else {
214 for (const item of offer.RawUpgrades) {
215 if (item.ItemCount < 0) {
216 throw new Error(`negative quantity in trade offer`);
217 }
218 addMods(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]);
219 }
220 }
221 }
222 if (offer.MiscItems) {
223 for (const item of offer.MiscItems) {
224 if (item.ItemCount < 0) {
225 throw new Error(`negative quantity in trade offer`);
226 }
227 addMiscItem(inventory, item.ItemType, item.ItemCount * factor);
228 }
229 }
230 if (offer.Recipes) {
231 if (factor == 1) {
232 addRecipes(inventory, offer.Recipes);
233 } else {
234 for (const item of offer.Recipes) {
235 if (item.ItemCount < 0) {
236 throw new Error(`negative quantity in trade offer`);
237 }
238 addRecipes(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]);
239 }
240 }
241 }
242 if (offer.FusionTreasures) {
243 if (factor == 1) {
244 addFusionTreasures(inventory, offer.FusionTreasures);
245 } else {
246 for (const item of offer.FusionTreasures) {
247 if (item.ItemCount < 0) {
248 throw new Error(`negative quantity in trade offer`);
249 }
250 addFusionTreasures(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]);
251 }
252 }
253 }
254 if (offer.PremiumCredits) {
255 if (offer.PremiumCredits < 0) {
256 throw new Error(`negative quantity in trade offer`);
257 }
258 updateCurrency(inventory, offer.PremiumCredits * factor * -1, true);
259 }
260 };
261
262 const RARITY_TAX_RATES: Record<TRarity, number> = { COMMON: 2000, UNCOMMON: 4000, RARE: 8000, LEGENDARY: 1_000_000 };
263 const LEGENDARY_ARCANE_TAX_RATES: number[] = [100_000, 300_000, 600_000, 1_000_000, 1_500_000, 2_100_000];
264 const calcuateTax = (offer: ITradeOffer): number => {
265 let tax = 0;
266 if (offer.RandomUpgrades) {
267 tax += offer.RandomUpgrades.length * 8000;
268 }
269 if (offer.Upgrades) {
270 for (const item of offer.Upgrades) {
271 if (item.ItemType in ExportArcanes) {
272 const rarity = ExportArcanes[item.ItemType].rarity;
273 if (rarity == "LEGENDARY") {
274 tax += LEGENDARY_ARCANE_TAX_RATES[(JSON.parse(item.UpgradeFingerprint!) as { lvl: number }).lvl];
275 }
276 } else {
277 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
278 tax += RARITY_TAX_RATES[ExportUpgrades[item.ItemType]?.rarity ?? "COMMON"];
279 }
280 }
281 }
282 if (offer.RawUpgrades) {
283 for (const item of offer.RawUpgrades) {
284 tax +=
285 RARITY_TAX_RATES[
286 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
287 ExportUpgrades[item.ItemType]?.rarity ?? ExportArcanes[item.ItemType]?.rarity ?? "COMMON"
288 ] * item.ItemCount;
289 }
290 }
291 if (offer.MiscItems) {
292 for (const item of offer.MiscItems) {
293 if (
294 item.ItemType.startsWith("/Lotus/Types/Game/Projections/T3") ||
295 item.ItemType.startsWith("/Lotus/Types/Game/Projections/T4")
296 ) {
297 tax += 4000 * item.ItemCount;
298 } else {
299 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
300 tax += RARITY_TAX_RATES[ExportResources[item.ItemType]?.rarity ?? "COMMON"] * item.ItemCount;
301 }
302 }
303 }
304 if (offer.Recipes) {
305 tax += offer.Recipes.length * 2000;
306 }
307 if (offer.FusionTreasures) {
308 for (const item of offer.FusionTreasures) {
309 tax += item.ItemCount * (item.Sockets ? 12000 : 4000);
310 }
311 }
312 tax += (offer.PremiumCredits ?? 0) * 500;
313 return tax;
314 };
315
316 const chargeTax = async (
317 inventory: TInventoryDatabaseDocument,
318 offer: ITradeOffer,
319 clanTaxInt: number,
320 guildId: string | undefined
321 ): Promise<void> => {
322 const tax = calcuateTax(offer);
323 const clanTaxRate = clanTaxInt ? 1 / clanTaxInt : 0;
324 const clanTax = clanTaxRate * tax;
325 updateCurrency(inventory, tax + clanTax, false);
326 if (guildId && clanTax) {
327 const guild = await Guild.findById(guildId);
328 if (guild) {
329 guild.VaultRegularCredits ??= 0;
330 guild.VaultRegularCredits += clanTax;
331 await guild.save();
332 }
333 }
334 };
Modified src/routes/api.ts +1 -0
@@ -372,6 +372,7 @@ apiRouter.post("/stepSequencers.php", stepSequencersController);
372 372 apiRouter.post("/syndicateSacrifice.php", syndicateSacrificeController);
373 373 apiRouter.post("/syndicateStandingBonus.php", syndicateStandingBonusController);
374 374 apiRouter.post("/tauntHistory.php", tauntHistoryController);
375 apiRouter.post("/trading.php", tradingController);
375 376 apiRouter.post("/trainingResult.php", trainingResultPostController);
376 377 apiRouter.post("/umbra.php", umbraController);
377 378 apiRouter.post("/unlockShipFeature.php", unlockShipFeatureController);
Modified src/services/importService.ts +2 -2
@@ -115,7 +115,7 @@ const convertWeaponSkin = (client: IWeaponSkinClient): IWeaponSkinDatabase => {
115 115 };
116 116 };
117 117
118 const convertUpgrade = (client: IUpgradeClient): IUpgradeDatabase => {
118 export const importUpgrade = (client: IUpgradeClient): IUpgradeDatabase => {
119 119 const { ItemId, ...rest } = client;
120 120 return {
121 121 ...rest,
@@ -266,7 +266,7 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial<
266 266 }
267 267 for (const key of ["Upgrades", "CrewShipSalvagedWeaponSkins", "CrewShipWeaponSkins"] as const) {
268 268 if (client[key] !== undefined) {
269 replaceArray<IUpgradeDatabase>(db[key], client[key].map(convertUpgrade));
269 replaceArray<IUpgradeDatabase>(db[key], client[key].map(importUpgrade));
270 270 }
271 271 }
272 272 for (const key of [
Modified src/services/inventoryService.ts +2 -1
@@ -1398,11 +1398,12 @@ export const updateCurrency = (
1398 1398 ): IInventoryChanges => {
1399 1399 if (price != 0 && isCurrencyTracked(inventory, usePremium)) {
1400 1400 if (usePremium) {
1401 if (inventory.PremiumCreditsFree > 0) {
1401 if (price > 0 && inventory.PremiumCreditsFree > 0) {
1402 1402 const premiumCreditsFreeDelta = Math.min(price, inventory.PremiumCreditsFree) * -1;
1403 1403 inventoryChanges.PremiumCreditsFree ??= 0;
1404 1404 inventoryChanges.PremiumCreditsFree += premiumCreditsFreeDelta;
1405 1405 inventory.PremiumCreditsFree += premiumCreditsFreeDelta;
1406 logger.debug(`spending ${-premiumCreditsFreeDelta} starter plat`);
1406 1407 }
1407 1408 inventoryChanges.PremiumCredits ??= 0;
1408 1409 inventoryChanges.PremiumCredits -= price;
Modified src/types/inventoryTypes/inventoryTypes.ts +11 -29
@@ -9,7 +9,7 @@ import type {
9 9 ILotusCustomization,
10 10 IShipCustomization
11 11 } from "./commonInventoryTypes.ts";
12 import type { IFingerprintStat, RivenFingerprint } from "../../helpers/rivenHelper.ts";
12 import type { IFingerprintStat } from "../../helpers/rivenHelper.ts";
13 13 import type { IOrbiterClient } from "../personalRoomsTypes.ts";
14 14 import type { ICountedStoreItem } from "warframe-public-export-plus";
15 15 import type { IEquipmentClient, IEquipmentDatabase, ITraits } from "../equipmentTypes.ts";
@@ -892,23 +892,22 @@ export interface IPendingTrade {
892 892 State: number;
893 893 SelfReady: boolean;
894 894 BuddyReady: boolean;
895 Giving?: IGiving;
895 Giving?: ITradeOffer;
896 896 Revision: number;
897 Getting: IGetting;
897 Getting: ITradeOffer;
898 898 ItemId: IOid;
899 899 ClanTax?: number;
900 900 }
901 901
902 export interface IGetting {
903 RandomUpgrades?: IRandomUpgrade[];
904 _SlotOrderInfo: GettingSlotOrderInfo[];
902 export interface ITradeOffer {
903 RandomUpgrades?: IUpgradeClient[];
904 Upgrades?: IUpgradeClient[];
905 RawUpgrades?: IRawUpgrade[];
906 MiscItems?: IMiscItem[];
907 Recipes?: ITypeCount[];
908 FusionTreasures?: IFusionTreasure[];
905 909 PremiumCredits?: number;
906 }
907
908 export interface IRandomUpgrade {
909 UpgradeFingerprint: RivenFingerprint;
910 ItemType: string;
911 ItemId: IOid;
910 _SlotOrderInfo: string[];
912 911 }
913 912
914 913 export interface IInnateDamageFingerprint {
@@ -931,23 +930,6 @@ export interface INemesisPetTargetFingerprint {
931 930 Name: string;
932 931 }
933 932
934 export enum GettingSlotOrderInfo {
935 Empty = "",
936 LotusUpgradesModsRandomizedPlayerMeleeWeaponRandomModRare0 = "/Lotus/Upgrades/Mods/Randomized/PlayerMeleeWeaponRandomModRare:0",
937 P = "P"
938 }
939
940 export interface IGiving {
941 RawUpgrades: ITypeCount[];
942 _SlotOrderInfo: GivingSlotOrderInfo[];
943 }
944
945 export enum GivingSlotOrderInfo {
946 Empty = "",
947 LotusTypesSentinelsSentinelPreceptsItemVacum = "/Lotus/Types/Sentinels/SentinelPrecepts/ItemVacum",
948 LotusUpgradesModsPistolDualStatElectEventPistolMod = "/Lotus/Upgrades/Mods/Pistol/DualStat/ElectEventPistolMod"
949 }
950
951 933 export interface IPeriodicMissionCompletionDatabase {
952 934 date: Date;
953 935 tag: string;