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 type { RequestHandler } from "express";
import {
    addGuildMemberMiscItemContribution,
    getGuildForRequestEx,
    getGuildVault,
    hasAccessToDojo,
    hasGuildPermission,
    processCompletedGuildTechProject,
    processFundedGuildTechProject,
    processGuildTechProjectContributionsUpdate,
    removePigmentsFromGuildMembers,
    scaleRequiredCount,
    setGuildTechLogState
} from "../../services/guildService.ts";
import { ExportDojoRecipes, ExportRailjackWeapons } from "warframe-public-export-plus";
import { getAccountForRequest, getBuildLabel } from "../../services/loginService.ts";
import {
    addCrewShipWeaponSkin,
    addEquipment,
    addItem,
    addMiscItem,
    addMiscItems,
    addRecipes,
    combineInventoryChanges,
    getInventory,
    getInventory2,
    occupySlot,
    updateCredits,
    updatePlatinum
} from "../../services/inventoryService.ts";
import type { IMiscItem } from "../../types/inventoryTypes/inventoryTypes.ts";
import { eInventorySlot } from "../../types/inventoryTypes/inventoryTypes.ts";
import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
import type { ITechProjectClient } from "../../types/guildTypes.ts";
import { eGuildPermission } from "../../types/guildTypes.ts";
import { GuildMember } from "../../models/guildModel.ts";
import { toMongoDate, toMongoDate2, toOid } from "../../helpers/inventoryHelpers.ts";
import { logger } from "../../utils/logger.ts";
import type { TInventoryDatabaseDocument } from "../../models/inventoryModels/inventoryModel.ts";
import { broadcastGuildUpdate } from "../../services/wsService.ts";

export const guildTechController: RequestHandler = async (req, res) => {
    const account = await getAccountForRequest(req);
    const accountId = account._id.toString();
    const data = JSON.parse(String(req.body)) as TGuildTechRequest;
    logger.trace("guildTech request:", data);
    if (data.Action == "Sync") {
        let needSave = false;
        const techProjects: ITechProjectClient[] = [];
        const buildLabel = getBuildLabel(req, account);
        const inventory = await getInventory2(accountId, "GuildId");
        const guild = await getGuildForRequestEx(req, inventory);
        if (guild.TechProjects) {
            for (const project of guild.TechProjects) {
                const techProject: ITechProjectClient = {
                    ItemType: project.ItemType,
                    ReqCredits: project.ReqCredits,
                    ReqItems: project.ReqItems,
                    State: project.State
                };
                if (project.CompletionDate) {
                    techProject.CompletionDate = toMongoDate2(project.CompletionDate, buildLabel);
                    if (
                        Date.now() >= project.CompletionDate.getTime() &&
                        setGuildTechLogState(guild, project.ItemType, 4, project.CompletionDate)
                    ) {
                        processCompletedGuildTechProject(guild, project.ItemType);
                        needSave = true;
                    }
                }
                techProjects.push(techProject);
            }
        }
        if (needSave) {
            await guild.save();
        }
        res.json({ TechProjects: techProjects });
    } else if (data.Action == "Start") {
        if (data.Mode != "Personal") {
            const inventory = await getInventory2(accountId, "GuildId", "LevelKeys");
            const guild = await getGuildForRequestEx(req, inventory);
            if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, eGuildPermission.Tech))) {
                res.status(400).send("-1").end();
                return;
            }
            const recipeType: string = (data.RecipeType ??
                data.ResearchId ??
                (req.query.researchId as string | undefined))!;
            const recipe = ExportDojoRecipes.research[recipeType];
            guild.TechProjects ??= [];
            if (!guild.TechProjects.find(x => x.ItemType == recipeType)) {
                const techProject =
                    guild.TechProjects[
                        guild.TechProjects.push({
                            ItemType: recipeType,
                            ReqCredits: guild.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, recipe.price),
                            ReqItems: recipe.ingredients.map(x => ({
                                ItemType: x.ItemType,
                                ItemCount: guild.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, x.ItemCount)
                            })),
                            State: 0
                        }) - 1
                    ];
                setGuildTechLogState(guild, techProject.ItemType, 5);
                if (guild.noDojoResearchCosts) {
                    processFundedGuildTechProject(guild, techProject, recipe);
                } else {
                    if (recipeType.substring(0, 39) == "/Lotus/Types/Items/Research/DojoColors/") {
                        guild.ActiveDojoColorResearch = recipeType;
                    }
                }
            }
            await guild.save();
            res.end();
            broadcastGuildUpdate(req, guild._id.toString());
        } else {
            const inventory = await getInventory2(
                accountId,
                "GuildId",
                "LevelKeys",
                "CrewShipSalvagedWeapons",
                "CrewShipSalvagedWeaponSkins",
                "PersonalTechProjects"
            );
            const recipe = ExportDojoRecipes.research[data.RecipeType!];
            if (data.TechProductCategory) {
                if (
                    data.TechProductCategory != "CrewShipWeapons" &&
                    data.TechProductCategory != "CrewShipWeaponSkins"
                ) {
                    throw new Error(`unexpected TechProductCategory: ${data.TechProductCategory}`);
                }
                if (!inventory[getSalvageCategory(data.TechProductCategory)].id(data.CategoryItemId!)) {
                    throw new Error(
                        `no item with id ${data.CategoryItemId} in ${getSalvageCategory(data.TechProductCategory)} array`
                    );
                }
            }
            const techProject =
                inventory.PersonalTechProjects[
                    inventory.PersonalTechProjects.push({
                        State: 0,
                        ReqCredits: recipe.price,
                        ItemType: data.RecipeType,
                        ProductCategory: data.TechProductCategory,
                        CategoryItemId: data.CategoryItemId,
                        ReqItems: recipe.ingredients
                    }) - 1
                ];
            await inventory.save();
            res.json({
                isPersonal: true,
                action: "Start",
                personalTech: techProject.toJSON()
            });
        }
    } else if (data.Action == "Contribute") {
        if (
            ("guildId" in req.query && (req.query.guildId as string) == "000000000000000000000000") ||
            (data.RecipeType && isPersonalResearch(data.RecipeType)) // For U28: https://onlyg.it/OpenWF/SpaceNinjaServer/issues/4312
        ) {
            const inventory = await getInventory2(
                accountId,
                "PersonalTechProjects",
                "infiniteCredits",
                "RegularCredits",
                "MiscItems"
            );
            const techProject = inventory.PersonalTechProjects.id(data.ResearchId!)!;

            techProject.ReqCredits -= data.RegularCredits;
            const inventoryChanges: IInventoryChanges = updateCredits(inventory, data.RegularCredits);

            const miscItemChanges = [];
            for (const miscItem of data.MiscItems) {
                const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType);
                if (reqItem) {
                    if (miscItem.ItemCount > reqItem.ItemCount) {
                        miscItem.ItemCount = reqItem.ItemCount;
                    }
                    reqItem.ItemCount -= miscItem.ItemCount;
                    miscItemChanges.push({
                        ItemType: miscItem.ItemType,
                        ItemCount: miscItem.ItemCount * -1
                    });
                }
            }
            addMiscItems(inventory, miscItemChanges);
            inventoryChanges.MiscItems = miscItemChanges;

            techProject.HasContributions = true;

            if (techProject.ReqCredits == 0 && !techProject.ReqItems.find(x => x.ItemCount > 0)) {
                techProject.State = 1;
                const recipe = ExportDojoRecipes.research[techProject.ItemType];
                techProject.CompletionDate = new Date(Date.now() + recipe.time * 1000);
            }

            await inventory.save();
            res.json({
                InventoryChanges: inventoryChanges,
                PersonalResearch: { $oid: data.ResearchId },
                PersonalResearchDate: techProject.CompletionDate ? toMongoDate(techProject.CompletionDate) : undefined
            });
        } else {
            const inventory = await getInventory2(
                accountId,
                "LevelKeys",
                "GuildId",
                "MiscItems",
                "infiniteCredits",
                "RegularCredits"
            );
            if (!hasAccessToDojo(inventory)) {
                res.status(400).send("-1").end();
                return;
            }

            const guild = await getGuildForRequestEx(req, inventory);
            const guildMember = (await GuildMember.findOne(
                { accountId, guildId: guild._id },
                "RegularCreditsContributed MiscItemsContributed"
            ))!;
            const recipeType: string = (data.RecipeType ??
                data.ResearchId ??
                (req.query.researchId as string | undefined))!;
            const techProject = guild.TechProjects!.find(x => x.ItemType == recipeType)!;

            if (data.VaultCredits) {
                if (data.VaultCredits > techProject.ReqCredits) {
                    data.VaultCredits = techProject.ReqCredits;
                }
                techProject.ReqCredits -= data.VaultCredits;
                guild.VaultRegularCredits! -= data.VaultCredits;
            }

            if (data.RegularCredits > techProject.ReqCredits) {
                data.RegularCredits = techProject.ReqCredits;
            }
            techProject.ReqCredits -= data.RegularCredits;

            guildMember.RegularCreditsContributed ??= 0;
            guildMember.RegularCreditsContributed += data.RegularCredits;

            if (data.VaultMiscItems?.length) {
                for (const miscItem of data.VaultMiscItems) {
                    const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType);
                    if (reqItem) {
                        if (miscItem.ItemCount > reqItem.ItemCount) {
                            miscItem.ItemCount = reqItem.ItemCount;
                        }
                        reqItem.ItemCount -= miscItem.ItemCount;

                        const vaultMiscItem = guild.VaultMiscItems!.find(x => x.ItemType == miscItem.ItemType)!;
                        vaultMiscItem.ItemCount -= miscItem.ItemCount;
                    }
                }
            }

            const miscItemChanges = [];
            for (const miscItem of data.MiscItems) {
                const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType);
                if (reqItem) {
                    if (miscItem.ItemCount > reqItem.ItemCount) {
                        miscItem.ItemCount = reqItem.ItemCount;
                    }
                    reqItem.ItemCount -= miscItem.ItemCount;
                    miscItemChanges.push({
                        ItemType: miscItem.ItemType,
                        ItemCount: miscItem.ItemCount * -1
                    });

                    addGuildMemberMiscItemContribution(guildMember, miscItem);
                }
            }
            addMiscItems(inventory, miscItemChanges);
            const inventoryChanges: IInventoryChanges = updateCredits(inventory, data.RegularCredits);
            inventoryChanges.MiscItems = miscItemChanges;

            // Check if research is fully funded now.
            await processGuildTechProjectContributionsUpdate(guild, techProject);

            await Promise.all([guild.save(), inventory.save(), guildMember.save()]);
            const buildLabel = getBuildLabel(req, account);
            res.json({
                InventoryChanges: inventoryChanges,
                Vault: await getGuildVault(guild, buildLabel)
            });
            broadcastGuildUpdate(req, guild._id.toString());
        }
    } else if (data.Action.split(",")[0] == "Buy") {
        const purchase = data as IGuildTechBuyRequest;
        if (purchase.Mode != "Personal") {
            const recipeType: string = (purchase.RecipeType ??
                purchase.ResearchId ??
                (req.query.researchId as string | undefined))!;
            const inventory = await getInventory2(
                accountId,
                "LevelKeys",
                "GuildId",
                "Recipes",
                "infiniteCredits",
                "RegularCredits"
            );
            const guild = await getGuildForRequestEx(req, inventory);
            if (
                !hasAccessToDojo(inventory) ||
                !(await hasGuildPermission(guild, accountId, eGuildPermission.Fabricator))
            ) {
                res.status(400).send("-1").end();
                return;
            }
            const quantity = parseInt(data.Action.split(",")[1]) || 1;
            const recipeChanges = [
                {
                    ItemType: recipeType,
                    ItemCount: quantity
                }
            ];
            addRecipes(inventory, recipeChanges);
            const currencyChanges = updateCredits(inventory, ExportDojoRecipes.research[recipeType].replicatePrice);
            await inventory.save();
            // Not a mistake: This response uses `inventoryChanges` instead of `InventoryChanges`.
            res.json({
                inventoryChanges: {
                    ...currencyChanges,
                    Recipes: recipeChanges
                }
            });
        } else {
            const inventory = await getInventory2(
                accountId,
                "PersonalTechProjects",
                "CrewShipWeapons",
                "CrewShipWeaponSkins",
                "CrewShipSalvagedWeapons",
                "CrewShipSalvagedWeaponSkins",
                "CrewShipSalvageBin"
            );
            const inventoryChanges = claimSalvagedComponent(inventory, purchase.CategoryItemId!);
            await inventory.save();
            res.json({
                inventoryChanges: inventoryChanges
            });
        }
    } else if (data.Action == "Fabricate") {
        const inventory = await getInventory(accountId, undefined);
        const guild = await getGuildForRequestEx(req, inventory);
        if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, eGuildPermission.Fabricator))) {
            res.status(400).send("-1").end();
            return;
        }
        const recipe = ExportDojoRecipes.fabrications[data.RecipeType!];
        const inventoryChanges: IInventoryChanges = updateCredits(inventory, recipe.price);
        inventoryChanges.MiscItems = recipe.ingredients.map(x => ({
            ItemType: x.ItemType,
            ItemCount: x.ItemCount * -1
        }));
        addMiscItems(inventory, inventoryChanges.MiscItems);
        combineInventoryChanges(inventoryChanges, await addItem(inventory, recipe.resultType));
        await inventory.save();
        // Not a mistake: This response uses `inventoryChanges` instead of `InventoryChanges`.
        res.json({ inventoryChanges: inventoryChanges });
    } else if (data.Action == "Pause") {
        const inventory = await getInventory2(accountId, "GuildId", "LevelKeys");
        const guild = await getGuildForRequestEx(req, inventory);
        if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, eGuildPermission.Tech))) {
            res.status(400).send("-1").end();
            return;
        }
        const project = guild.TechProjects!.find(x => x.ItemType == data.RecipeType)!;
        project.State = -2;
        guild.ActiveDojoColorResearch = "";
        await guild.save();
        await removePigmentsFromGuildMembers(guild._id);
        res.end();
    } else if (data.Action == "Unpause") {
        const inventory = await getInventory2(accountId, "GuildId", "LevelKeys");
        const guild = await getGuildForRequestEx(req, inventory);
        if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, eGuildPermission.Tech))) {
            res.status(400).send("-1").end();
            return;
        }
        const project = guild.TechProjects!.find(x => x.ItemType == data.RecipeType)!;
        project.State = 0;
        guild.ActiveDojoColorResearch = data.RecipeType!;
        await guild.save();
        res.end();
    } else if (data.Action == "Cancel" && data.CategoryItemId) {
        const inventory = await getInventory2(
            accountId,
            "PersonalTechProjects",
            "infiniteCredits",
            "RegularCredits",
            "MiscItems"
        );
        const personalTechProjectIndex = inventory.PersonalTechProjects.findIndex(x =>
            x.CategoryItemId?.equals(data.CategoryItemId)
        );
        const personalTechProject = inventory.PersonalTechProjects[personalTechProjectIndex];
        inventory.PersonalTechProjects.splice(personalTechProjectIndex, 1);

        const meta = ExportDojoRecipes.research[personalTechProject.ItemType];
        const contributedCredits = meta.price - personalTechProject.ReqCredits;
        const inventoryChanges = updateCredits(inventory, contributedCredits * -1);
        inventoryChanges.MiscItems = [];
        for (const ingredient of meta.ingredients) {
            const reqItem = personalTechProject.ReqItems.find(x => x.ItemType == ingredient.ItemType);
            if (reqItem) {
                const contributedItems = ingredient.ItemCount - reqItem.ItemCount;
                inventoryChanges.MiscItems.push({
                    ItemType: ingredient.ItemType,
                    ItemCount: contributedItems
                });
            }
        }
        addMiscItems(inventory, inventoryChanges.MiscItems);

        await inventory.save();
        res.json({
            action: "Cancel",
            isPersonal: true,
            inventoryChanges: inventoryChanges,
            personalTech: {
                ItemId: toOid(personalTechProject._id)
            }
        });
    } else if (data.Action == "Rush" && data.CategoryItemId) {
        const inventory = await getInventory2(
            accountId,
            "infinitePlatinum",
            "PremiumCredits",
            "PremiumCreditsFree",
            "PersonalTechProjects",
            "CrewShipSalvageBin",
            "CrewShipWeapons",
            "CrewShipSalvagedWeapons",
            "CrewShipWeaponSkins",
            "CrewShipSalvagedWeaponSkins"
        );
        const inventoryChanges: IInventoryChanges = {
            ...updatePlatinum(inventory, 20),
            ...claimSalvagedComponent(inventory, data.CategoryItemId)
        };
        await inventory.save();
        res.json({
            inventoryChanges: inventoryChanges
        });
    } else if (data.Action == "InstantFinish") {
        if (data.TechProductCategory != "CrewShipWeapons" && data.TechProductCategory != "CrewShipWeaponSkins") {
            throw new Error(`unexpected TechProductCategory: ${data.TechProductCategory}`);
        }
        const inventory = await getInventory2(
            accountId,
            "PersonalTechProjects",
            "CrewShipSalvageBin",
            "CrewShipWeapons",
            "CrewShipSalvagedWeapons",
            "CrewShipWeaponSkins",
            "CrewShipSalvagedWeaponSkins",
            "MiscItems"
        );
        const inventoryChanges = finishComponentRepair(inventory, data.TechProductCategory, data.CategoryItemId!);
        addMiscItem(inventory, "/Lotus/Types/Items/MiscItems/InstantSalvageRepairItem", -1, inventoryChanges);
        await inventory.save();
        res.json({
            inventoryChanges: inventoryChanges
        });
    } else {
        logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
        throw new Error(`unhandled guildTech request`);
    }
};

type TGuildTechRequest =
    | { Action: "Sync" | "SomethingElseThatWeMightNotKnowAbout" }
    | IGuildTechBasicRequest
    | IGuildTechContributeRequest;

interface IGuildTechBasicRequest {
    Action: "Start" | "Fabricate" | "Pause" | "Unpause" | "Cancel" | "Rush" | "InstantFinish";
    Mode?: "Guild" | "Personal";
    ResearchId?: string; // U18.0.6 uses that instead RecipeType
    RecipeType?: string;
    TechProductCategory?: string;
    CategoryItemId?: string;
}

interface IGuildTechBuyRequest extends Omit<IGuildTechBasicRequest, "Action"> {
    Action: string;
}

interface IGuildTechContributeRequest {
    Action: "Contribute";
    ResearchId?: string;
    RecipeType?: string;
    RegularCredits: number;
    MiscItems: IMiscItem[];
    VaultCredits?: number;
    VaultMiscItems?: IMiscItem[];
}

const isPersonalResearch = (uniqueName: string): boolean | null => {
    if (!(uniqueName in ExportDojoRecipes.research)) {
        return null;
    }
    return !!ExportDojoRecipes.research[uniqueName].resultType;
};

const getSalvageCategory = (
    category: "CrewShipWeapons" | "CrewShipWeaponSkins"
): "CrewShipSalvagedWeapons" | "CrewShipSalvagedWeaponSkins" => {
    return category == "CrewShipWeapons" ? "CrewShipSalvagedWeapons" : "CrewShipSalvagedWeaponSkins";
};

const claimSalvagedComponent = (
    inventory: Pick<
        TInventoryDatabaseDocument,
        | "PersonalTechProjects"
        | "CrewShipWeapons"
        | "CrewShipWeaponSkins"
        | "CrewShipSalvagedWeapons"
        | "CrewShipSalvagedWeaponSkins"
        | "CrewShipSalvageBin"
    >,
    itemId: string
): IInventoryChanges => {
    // delete personal tech project
    const personalTechProjectIndex = inventory.PersonalTechProjects.findIndex(x => x.CategoryItemId?.equals(itemId));
    const personalTechProject = inventory.PersonalTechProjects[personalTechProjectIndex];
    inventory.PersonalTechProjects.splice(personalTechProjectIndex, 1);

    const category = personalTechProject.ProductCategory! as "CrewShipWeapons" | "CrewShipWeaponSkins";
    return finishComponentRepair(inventory, category, itemId);
};

const finishComponentRepair = (
    inventory: Pick<
        TInventoryDatabaseDocument,
        | "CrewShipWeapons"
        | "CrewShipWeaponSkins"
        | "CrewShipSalvagedWeapons"
        | "CrewShipSalvagedWeaponSkins"
        | "CrewShipSalvageBin"
    >,
    category: "CrewShipWeapons" | "CrewShipWeaponSkins",
    itemId: string
): IInventoryChanges => {
    const salvageCategory = getSalvageCategory(category);

    // find salved part & delete it
    const salvageIndex = inventory[salvageCategory].findIndex(x => x._id.equals(itemId));
    const salvageItem = inventory[salvageCategory][salvageIndex];
    inventory[salvageCategory].splice(salvageIndex, 1);

    // add final item
    const inventoryChanges = {
        ...(category == "CrewShipWeaponSkins"
            ? addCrewShipWeaponSkin(inventory, salvageItem.ItemType, salvageItem.UpgradeFingerprint)
            : addEquipment(inventory, category, salvageItem.ItemType, {
                  UpgradeType: ExportRailjackWeapons[salvageItem.ItemType].defaultUpgrades?.[0].ItemType,
                  UpgradeFingerprint: salvageItem.UpgradeFingerprint
              })),
        ...occupySlot(inventory, eInventorySlot.RJ_COMPONENT_AND_ARMAMENTS, false)
    };

    inventoryChanges.RemovedIdItems = [
        {
            ItemId: { $oid: itemId }
        }
    ];

    return inventoryChanges;
};

export const selfTestGuildTech = (): boolean => {
    let allGood = true;

    if (isPersonalResearch("/Lotus/Types/Items/ShipFeatureItems/Railjack/RailjackHullFeatureItemBlueprint") !== true) {
        logger.warn(`isPersonalResearch self-test failed for RailjackHullFeatureItemBlueprint`);
        allGood = false;
    }

    if (isPersonalResearch("/Lotus/Types/Recipes/Railjack/Weapons/MissileLauncherEMPTierABlueprint") !== false) {
        logger.warn(`isPersonalResearch self-test failed for MissileLauncherEMPTierABlueprint`);
        allGood = false;
    }

    if (isPersonalResearch("I am not a recipe") !== null) {
        logger.warn(`isPersonalResearch self-test failed for non-existent recipe`);
        allGood = false;
    }

    return allGood;
};