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

XFESpaceNinjaServer

A simple server for a small space ninja game

公开
关注 0 Fork 0 Star 0
UTF-8
import { getGuildForRequestEx, hasAccessToDojo, hasGuildPermission } from "../../services/guildService.ts";
import {
    addFusionTreasures,
    addMiscItem,
    addMods,
    addRecipes,
    freeUpSlot,
    getInventory2,
    occupySlot,
    updateCredits,
    updatePlatinum
} from "../../services/inventoryService.ts";
import { getAccountForRequest, getBuildLabel } from "../../services/loginService.ts";
import { eGuildPermission } from "../../types/guildTypes.ts";
import type { RequestHandler } from "express";
import type { IPendingTradeDatabase, ITradeOffer } from "../../types/tradingTypes.ts";
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
import { logger } from "../../utils/logger.ts";
import { fromMongoDate, fromOid, version_compare } from "../../helpers/inventoryHelpers.ts";
import gameToBuildVersion from "../../constants/gameToBuildVersion.ts";
import type { TInventoryDatabaseDocument } from "../../models/inventoryModels/inventoryModel.ts";
import { importEquipment, importUpgrade } from "../../services/importService.ts";
import { Guild } from "../../models/guildModel.ts";
import { ExportArcanes, ExportResources, ExportUpgrades, type TRarity } from "warframe-public-export-plus";
import { getInfNodes, getNemesisManifest } from "../../helpers/nemesisHelpers.ts";
import { PendingTrade } from "../../models/tradingModel.ts";
import type { HydratedDocument, QueryFilter } from "mongoose";
import { exportTrade } from "../../services/tradingService.ts";
import { eInventorySlot } from "../../types/inventoryTypes/inventoryTypes.ts";

export const tradingController: RequestHandler = async (req, res) => {
    const account = await getAccountForRequest(req);
    const accountId = account._id.toString();
    const op = req.query.op as string as TTradingOperation;
    if (op == eTradingOperation.InitialOffer) {
        const buddyId = req.query.buddyId as string;
        const offer = getJSONfromString<ITradeOffer>(String(req.body));
        const us = getSide(accountId, buddyId);
        const them = us == "a" ? "b" : "a";
        const trade = await PendingTrade.create({
            [us]: accountId,
            [them]: buddyId,
            [`${us}Offer`]: offer,
            clanTax:
                (req.query.guildId ? await Guild.findById(req.query.guildId as string, "TradeTax") : undefined)
                    ?.TradeTax ?? 10 // When trading in Maroo's Bazaar, a 10% "Hub Tax" applies.
        });
        res.json({
            PendingTrades: [exportTrade(trade, us)]
        });
    } else if (op == eTradingOperation.CounterOffer) {
        const buddyId = req.query.buddyId as string;
        const offer = getJSONfromString<ITradeOffer>(String(req.body));
        const us = getSide(accountId, buddyId);
        const trade = (await PendingTrade.findOneAndUpdate(
            getTradeFilter(accountId, buddyId),
            {
                $set: {
                    [`${us}Offer`]: offer
                },
                $inc: {
                    revision: 1
                }
            },
            { returnDocument: "after" }
        ))!;
        res.json({
            PendingTrades: [exportTrade(trade, us)]
        });
    } else if (op == eTradingOperation.AcceptTrade) {
        const buildLabel = getBuildLabel(req, account);
        const buddyId = req.query.buddyId as string;
        const trade = (await getPendingTrade(accountId, buddyId))!;
        if (parseInt(req.query.revision as string) != trade.revision) {
            throw new Error(`can't accept trade because it was unexpectedly revised`);
        }
        const us = trade.a.equals(accountId) ? "a" : "b";
        trade[`${us}Accepted`] = true;
        const them = us == "a" ? "b" : "a";
        if (trade[`${them}Accepted`]) {
            const [aInventory, bInventory] = await Promise.all([
                getInventory2(
                    trade.a,
                    "tradesDontTouchInventory",
                    "Upgrades",
                    "RawUpgrades",
                    "MiscItems",
                    "Recipes",
                    "LongGuns",
                    "Pistols",
                    "Melee",
                    "WeaponBin",
                    "HybridFusionTreasures",
                    "NemesisHistory",
                    "Nemesis",
                    "infinitePlatinum",
                    "PremiumCreditsFree",
                    "PremiumCredits",
                    "infiniteCredits",
                    "RegularCredits",
                    "infiniteTrades",
                    "TradesRemaining"
                ),
                getInventory2(
                    trade.b,
                    "tradesDontTouchInventory",
                    "Upgrades",
                    "RawUpgrades",
                    "MiscItems",
                    "Recipes",
                    "LongGuns",
                    "Pistols",
                    "Melee",
                    "WeaponBin",
                    "HybridFusionTreasures",
                    "NemesisHistory",
                    "Nemesis",
                    "infinitePlatinum",
                    "PremiumCreditsFree",
                    "PremiumCredits",
                    "infiniteCredits",
                    "RegularCredits",
                    "infiniteTrades",
                    "TradesRemaining"
                )
            ]);

            if (!aInventory.tradesDontTouchInventory) {
                applyOfferToInventory(aInventory, trade.aOffer, -1); // A gives up their items
                applyOfferToInventory(aInventory, trade.bOffer, +1); // to get what B offered
                await chargeTax(aInventory, trade.bOffer, trade.clanTax, req.query.guildId as string | undefined); // and pays tax for it
                if (!aInventory.infiniteTrades) {
                    aInventory.TradesRemaining -= 1;
                }
            }
            if (!bInventory.tradesDontTouchInventory) {
                applyOfferToInventory(bInventory, trade.bOffer, -1); // Conversely, B gives up their items
                applyOfferToInventory(bInventory, trade.aOffer, +1); // to get what A offered
                await chargeTax(bInventory, trade.aOffer, trade.clanTax, req.query.guildId as string | undefined); // and pays tax for it
                if (!bInventory.infiniteTrades) {
                    bInventory.TradesRemaining -= 1;
                }
            }

            await Promise.all([aInventory.save(), bInventory.save(), trade.deleteOne()]);
            res.send(version_compare(buildLabel, gameToBuildVersion["40.0.0"]) < 0 ? "9" : "12").end();
        } else {
            await trade.save();
            res.send(version_compare(buildLabel, gameToBuildVersion["40.0.0"]) < 0 ? "8" : "11").end();
        }
    } else if (op == eTradingOperation.CancelTrade) {
        const buddyId = req.query.buddyId as string;
        await PendingTrade.deleteOne(getTradeFilter(accountId, buddyId));
        res.end();
    } else if (op == eTradingOperation.RefreshTradeInfo) {
        const buddyId = req.query.buddyId as string;
        const trade = (await getPendingTrade(accountId, buddyId))!;
        const us = trade.a.equals(accountId) ? "a" : "b";
        res.json({
            PendingTrades: [exportTrade(trade, us)]
        });
    } else if (op == eTradingOperation.SetClanTax) {
        const inventory = await getInventory2(accountId, "GuildId", "LevelKeys");
        const guild = await getGuildForRequestEx(req, inventory);
        if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, eGuildPermission.Treasurer))) {
            res.status(400).send("-1").end();
            return;
        }
        guild.TradeTax = parseInt(req.query.tax as string);
        await guild.save();
        res.send(guild.TradeTax).end();
    } else if (op == eTradingOperation.SetReady) {
        const buddyId = req.query.buddyId as string;
        const us = getSide(accountId, buddyId);
        const trade = (await getPendingTrade(accountId, buddyId))!;
        if (trade.revision == parseInt(req.query.revision as string)) {
            trade[`${us}Ready`] = true;
            await trade.save();
        }
        res.json({
            PendingTrades: [exportTrade(trade, us)]
        });
    } else if (op == eTradingOperation.UnsetReady) {
        const buddyId = req.query.buddyId as string;
        const us = getSide(accountId, buddyId);
        const trade = (await PendingTrade.findOneAndUpdate(
            getTradeFilter(accountId, buddyId),
            { [`${us}Ready`]: false, aAccepted: false, bAccepted: false },
            { returnDocument: "after" }
        ))!;
        res.json({
            PendingTrades: [exportTrade(trade, us)]
        });
    } else {
        if (req.body) {
            logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
        }
        throw new Error(`unknown trading op: ${op}`);
    }
};

const eTradingOperation = {
    InitialOffer: "0",
    CounterOffer: "1",
    AcceptTrade: "2",
    CancelTrade: "3",
    RefreshTradeInfo: "4",
    SetClanTax: "5",
    SetReady: "6",
    UnsetReady: "7",
    NeverSayNever: "01189998819991197253"
} as const;
type TTradingOperation = (typeof eTradingOperation)[keyof typeof eTradingOperation];

const getSide = (accountId: string, buddyId: string): "a" | "b" => {
    return accountId.localeCompare(buddyId) < 0 ? "a" : "b";
};

const getTradeFilter = (accountId: string, buddyId: string): QueryFilter<IPendingTradeDatabase> => {
    return accountId.localeCompare(buddyId) < 0 ? { a: accountId, b: buddyId } : { a: buddyId, b: accountId };
};

const getPendingTrade = (
    accountId: string,
    buddyId: string
): Promise<HydratedDocument<IPendingTradeDatabase> | null> => {
    return PendingTrade.findOne(getTradeFilter(accountId, buddyId));
};

const applyOfferToInventory = (
    inventory: Pick<
        TInventoryDatabaseDocument,
        | "Upgrades"
        | "RawUpgrades"
        | "MiscItems"
        | "Recipes"
        | "LongGuns"
        | "Pistols"
        | "Melee"
        | "WeaponBin"
        | "HybridFusionTreasures"
        | "NemesisHistory"
        | "Nemesis"
        | "infinitePlatinum"
        | "PremiumCreditsFree"
        | "PremiumCredits"
    >,
    offer: ITradeOffer,
    factor: 1 | -1
): void => {
    if (offer.RandomUpgrades) {
        for (const item of offer.RandomUpgrades) {
            if (factor == -1) {
                inventory.Upgrades.pull({ _id: fromOid(item.ItemId) });
            } else {
                inventory.Upgrades.push(importUpgrade(item));
            }
        }
    }
    if (offer.Upgrades) {
        for (const item of offer.Upgrades) {
            if (factor == -1) {
                inventory.Upgrades.pull({ _id: fromOid(item.ItemId) });
            } else {
                inventory.Upgrades.push(importUpgrade(item));
            }
        }
    }
    if (offer.RawUpgrades) {
        if (factor == 1) {
            addMods(inventory, offer.RawUpgrades);
        } else {
            for (const item of offer.RawUpgrades) {
                if (item.ItemCount < 0) {
                    throw new Error(`negative quantity in trade offer`);
                }
                addMods(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]);
            }
        }
    }
    if (offer.MiscItems) {
        for (const item of offer.MiscItems) {
            if (item.ItemCount < 0) {
                throw new Error(`negative quantity in trade offer`);
            }
            addMiscItem(inventory, item.ItemType, item.ItemCount * factor);
        }
    }
    if (offer.Recipes) {
        if (factor == 1) {
            addRecipes(inventory, offer.Recipes);
        } else {
            for (const item of offer.Recipes) {
                if (item.ItemCount < 0) {
                    throw new Error(`negative quantity in trade offer`);
                }
                addRecipes(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]);
            }
        }
    }
    for (const key of ["LongGuns", "Pistols", "Melee"] as const) {
        if (offer[key]) {
            for (const item of offer[key]) {
                if (factor == 1) {
                    inventory[key].push(importEquipment(item));
                    occupySlot(inventory, eInventorySlot.WEAPONS, false);
                } else {
                    inventory[key].pull({ _id: fromOid(item.ItemId) });
                    freeUpSlot(inventory, eInventorySlot.WEAPONS);
                }
            }
        }
    }
    if (offer.FusionTreasures) {
        if (factor == 1) {
            addFusionTreasures(inventory, offer.FusionTreasures);
        } else {
            for (const item of offer.FusionTreasures) {
                if (item.ItemCount < 0) {
                    throw new Error(`negative quantity in trade offer`);
                }
                addFusionTreasures(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]);
            }
        }
    }
    if (offer.NemesisHistory) {
        inventory.NemesisHistory ??= [];
        if (factor == 1) {
            if (inventory.Nemesis) {
                logger.warn(`overwriting an existing nemesis as a new one is being requested`);
            }
            const manifest = getNemesisManifest(offer.NemesisHistory.manifest);
            inventory.Nemesis = {
                fp: BigInt(offer.NemesisHistory.fp),
                manifest: offer.NemesisHistory.manifest,
                KillingSuit: offer.NemesisHistory.KillingSuit,
                killingDamageType: offer.NemesisHistory.killingDamageType,
                ShoulderHelmet: offer.NemesisHistory.ShoulderHelmet,
                WeaponIdx: offer.NemesisHistory.WeaponIdx,
                AgentIdx: offer.NemesisHistory.AgentIdx,
                BirthNode: offer.NemesisHistory.BirthNode,
                Faction: offer.NemesisHistory.Faction,
                Rank: 0,
                k: false,
                Traded: true,
                d: fromMongoDate(offer.NemesisHistory.d),
                InfNodes: getInfNodes(manifest, 0),
                GuessHistory: [],
                Hints: [],
                HintProgress: 0,
                Weakened: false,
                PrevOwners: offer.NemesisHistory.PrevOwners + 1,
                HenchmenKilled: 0,
                SecondInCommand: false,
                MissionCount: 0,
                LastEnc: 0
            };
        } else {
            const i = inventory.NemesisHistory.findIndex(x => x.fp == BigInt(offer.NemesisHistory!.fp));
            if (i != -1) {
                inventory.NemesisHistory.splice(i, 1);
            }
        }
    }
    if (offer.PremiumCredits) {
        if (offer.PremiumCredits < 0) {
            throw new Error(`negative quantity in trade offer`);
        }
        updatePlatinum(inventory, offer.PremiumCredits * factor * -1, true);
    }
};

const RARITY_TAX_RATES: Record<TRarity, number> = { COMMON: 2000, UNCOMMON: 4000, RARE: 8000, LEGENDARY: 1_000_000 };
const LEGENDARY_ARCANE_TAX_RATES: number[] = [100_000, 300_000, 600_000, 1_000_000, 1_500_000, 2_100_000];
const calcuateTax = (offer: ITradeOffer): number => {
    let tax = 0;
    if (offer.RandomUpgrades) {
        tax += offer.RandomUpgrades.length * 8000;
    }
    if (offer.Upgrades) {
        for (const item of offer.Upgrades) {
            if (item.ItemType in ExportArcanes) {
                const rarity = ExportArcanes[item.ItemType].rarity;
                if (rarity == "LEGENDARY") {
                    tax += LEGENDARY_ARCANE_TAX_RATES[(JSON.parse(item.UpgradeFingerprint!) as { lvl: number }).lvl];
                }
            } else {
                // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
                tax += RARITY_TAX_RATES[ExportUpgrades[item.ItemType]?.rarity ?? "COMMON"];
            }
        }
    }
    if (offer.RawUpgrades) {
        for (const item of offer.RawUpgrades) {
            tax +=
                RARITY_TAX_RATES[
                    // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
                    ExportUpgrades[item.ItemType]?.rarity ?? ExportArcanes[item.ItemType]?.rarity ?? "COMMON"
                ] * item.ItemCount;
        }
    }
    if (offer.MiscItems) {
        for (const item of offer.MiscItems) {
            if (
                item.ItemType.startsWith("/Lotus/Types/Game/Projections/T3") ||
                item.ItemType.startsWith("/Lotus/Types/Game/Projections/T4")
            ) {
                tax += 4000 * item.ItemCount;
            } else {
                // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
                tax += RARITY_TAX_RATES[ExportResources[item.ItemType]?.rarity ?? "COMMON"] * item.ItemCount;
            }
        }
    }
    tax += (offer.Recipes?.length ?? 0) * 2000;
    tax += (offer.LongGuns?.length ?? 0) * 2000;
    tax += (offer.Pistols?.length ?? 0) * 2000;
    tax += (offer.Melee?.length ?? 0) * 2000;
    if (offer.FusionTreasures) {
        for (const item of offer.FusionTreasures) {
            tax += item.ItemCount * (item.Sockets ? 12000 : 4000);
        }
    }
    if (offer.NemesisHistory) {
        tax += 8000;
    }
    tax += (offer.PremiumCredits ?? 0) * 500;
    return tax;
};

const chargeTax = async (
    inventory: Pick<TInventoryDatabaseDocument, "infiniteCredits" | "RegularCredits">,
    offer: ITradeOffer,
    clanTaxInt: number,
    guildId: string | undefined
): Promise<void> => {
    const tax = calcuateTax(offer);
    const clanTaxRate = clanTaxInt ? 1 / clanTaxInt : 0;
    const clanTax = clanTaxRate * tax;
    updateCredits(inventory, tax + clanTax);
    if (guildId && clanTax) {
        const guild = await Guild.findById(guildId);
        if (guild) {
            guild.VaultRegularCredits ??= 0;
            guild.VaultRegularCredits += clanTax;
            await guild.save();
        }
    }
};