From 2adbb6a5fe6f563d47d81082f1926fe2289f0d2e Mon Sep 17 00:00:00 2001 From: DrakiaXYZ Date: Sun, 25 Feb 2024 08:53:57 +0000 Subject: [PATCH] Properly take and return tools when crafting (!234) When starting a craft, tools are now taken, and the templateId is stored in the production in the user profile When finishing a craft, space for both the tools and crafted item is verified, then both are added to the player stash correctly flagged as non-FiR and FiR respectively Included a bit of code cleanup/reorg in areas I was working in A few assumptions were made: - Tools are expected to be single items, not stacks of an item (productions.json doesn't include a count property for tools, so this seems safe) - Tools will never be a preset or have child items - That the `canPlaceItemsInInventory` method over a concatenation of the tools and crafted item(s) will result in the same result as calling it individually over the two collections of items individually Will need tested once merged into 380, I did basic testing, but there's a lot of different crafts that require tools Co-authored-by: DrakiaXYZ <565558+TheDgtl@users.noreply.github.com> Reviewed-on: https://dev.sp-tarkov.com/SPT-AKI/Server/pulls/234 Co-authored-by: DrakiaXYZ Co-committed-by: DrakiaXYZ --- project/src/controllers/HideoutController.ts | 128 +++++++++++++----- project/src/helpers/HideoutHelper.ts | 12 +- .../src/models/eft/common/tables/IBotBase.ts | 2 + ...HideoutSingleProductionStartRequestData.ts | 1 + 4 files changed, 105 insertions(+), 38 deletions(-) diff --git a/project/src/controllers/HideoutController.ts b/project/src/controllers/HideoutController.ts index 68279e7e..4f8679d9 100644 --- a/project/src/controllers/HideoutController.ts +++ b/project/src/controllers/HideoutController.ts @@ -592,23 +592,31 @@ export class HideoutController const recipe = this.databaseServer.getTables().hideout.production.find((p) => p._id === body.recipeId); // Find the actual amount of items we need to remove because body can send weird data - const recipeRequirementsClone = this.jsonUtil.clone(recipe.requirements.filter((i) => i.type === "Item")); + const recipeRequirementsClone = this.jsonUtil.clone(recipe.requirements.filter((i) => i.type === "Item" || i.type === "Tool")); const output = this.eventOutputHolder.getOutput(sessionID); - - for (const itemToDelete of body.items) + const itemsToDelete = body.items.concat(body.tools); + for (const itemToDelete of itemsToDelete) { const itemToCheck = pmcData.Inventory.items.find((i) => i._id === itemToDelete.id); const requirement = recipeRequirementsClone.find((requirement) => requirement.templateId === itemToCheck._tpl ); - if (requirement.count <= 0) + + // Handle tools not having a `count`, but always only requiring 1 + const requiredCount = requirement.count ?? 1; + if (requiredCount <= 0) { continue; } - this.inventoryHelper.removeItemByCount(pmcData, itemToDelete.id, requirement.count, sessionID, output); - requirement.count -= itemToDelete.count; + this.inventoryHelper.removeItemByCount(pmcData, itemToDelete.id, requiredCount, sessionID, output); + + // Tools don't have a count + if (requirement.type !== "Tool") + { + requirement.count -= itemToDelete.count; + } } return output; @@ -793,6 +801,46 @@ export class HideoutController output: IItemEventRouterResponse, ): void { + // Validate that we have a matching production + const productionDict = Object.entries(pmcData.Hideout.Production); + let prodId: string; + for (const [productionId, production] of productionDict) + { + // Skip null production objects + if (!production) + { + continue; + } + + if (this.hideoutHelper.isProductionType(production)) + { + // Production or ScavCase + if (production.RecipeId === request.recipeId) + { + prodId = productionId; // Set to objects key + break; + } + } + } + + // If we're unable to find the production, send an error to the client + if (prodId === undefined) + { + this.logger.error( + this.localisationService.getText( + "hideout-unable_to_find_production_in_profile_by_recipie_id", + request.recipeId, + ), + ); + + this.httpResponse.appendErrorToOutput(output, this.localisationService.getText( + "hideout-unable_to_find_production_in_profile_by_recipie_id", + request.recipeId, + )); + + return; + } + // Variables for managemnet of skill let craftingExpAmount = 0; @@ -865,42 +913,29 @@ export class HideoutController } } - // Loops over all current productions on profile - we want to find a matching production - const productionDict = Object.entries(pmcData.Hideout.Production); - let prodId: string; - for (const production of productionDict) + // Build an array of the tools that need to be returned to the player + const toolsToSendToPlayer: Item[][] = []; + const production = pmcData.Hideout.Production[prodId]; + if (production.sptRequiredTools?.length > 0) { - // Skip null production objects - if (!production[1]) + for (const tool of production.sptRequiredTools) { - continue; - } + const toolToAdd: Item = { + _id: this.hashUtil.generate(), + _tpl: tool, + }; - if (this.hideoutHelper.isProductionType(production[1])) - { - // Production or ScavCase - if ((production[1] as Production).RecipeId === request.recipeId) + if (this.itemHelper.isItemTplStackable(tool)) { - prodId = production[0]; // Set to objects key - break; + toolToAdd.upd = { + StackObjectsCount: 1, + } } + + toolsToSendToPlayer.push([toolToAdd]); } } - if (prodId === undefined) - { - this.logger.error( - this.localisationService.getText( - "hideout-unable_to_find_production_in_profile_by_recipie_id", - request.recipeId, - ), - ); - - this.httpResponse.appendErrorToOutput(output); - - return; - } - // Check if the recipe is the same as the last one - get bonus when crafting same thing multiple times const area = pmcData.Hideout.Areas.find((area) => area.type === recipe.areaType); if (area && request.recipeId !== area.lastRecipe) @@ -920,15 +955,34 @@ export class HideoutController hoursCrafting -= this.hideoutConfig.hoursForSkillCrafting * multiplierCrafting; } - // Create request for what we want to add to stash + // Make sure we can fit both the craft result and tools in the stash + const totalResultItems = toolsToSendToPlayer.concat(itemAndChildrenToSendToPlayer); + if (!this.inventoryHelper.canPlaceItemsInInventory(sessionID, totalResultItems)) + { + this.httpResponse.appendErrorToOutput(output, this.localisationService.getText("inventory-no_stash_space")); + return; + } + + // Add the used tools to the stash as non-FiR + const addToolsRequest: IAddItemsDirectRequest = { + itemsWithModsToAdd: toolsToSendToPlayer, + foundInRaid: false, + useSortingTable: false, + callback: null, + }; + this.inventoryHelper.addItemsToStash(sessionID, addToolsRequest, pmcData, output); + if (output.warnings.length > 0) + { + return; + } + + // Add the crafting result to the stash, marked as FiR const addItemsRequest: IAddItemsDirectRequest = { itemsWithModsToAdd: itemAndChildrenToSendToPlayer, foundInRaid: true, useSortingTable: false, callback: null, }; - - // Add FiR crafted items(s) to player inventory this.inventoryHelper.addItemsToStash(sessionID, addItemsRequest, pmcData, output); if (output.warnings.length > 0) { diff --git a/project/src/helpers/HideoutHelper.ts b/project/src/helpers/HideoutHelper.ts index 09631ac9..a9da0695 100644 --- a/project/src/helpers/HideoutHelper.ts +++ b/project/src/helpers/HideoutHelper.ts @@ -95,11 +95,20 @@ export class HideoutHelper modifiedProductionTime = 40; } - pmcData.Hideout.Production[body.recipeId] = this.initProduction( + const production = this.initProduction( body.recipeId, modifiedProductionTime, recipe.needFuelForAllProductionTime, ); + + // Store the tools used for this production, so we can return them later + const productionTools = recipe.requirements.filter(req => req.type === "Tool").map(req => req.templateId); + if (productionTools.length > 0) + { + production.sptRequiredTools = productionTools; + } + + pmcData.Hideout.Production[body.recipeId] = production; } /** @@ -539,6 +548,7 @@ export class HideoutHelper recipeId: HideoutHelper.waterCollector, Action: "HideoutSingleProductionStart", items: [], + tools: [], timestamp: this.timeUtil.getTimestamp(), }; diff --git a/project/src/models/eft/common/tables/IBotBase.ts b/project/src/models/eft/common/tables/IBotBase.ts index accbcaba..16627b6a 100644 --- a/project/src/models/eft/common/tables/IBotBase.ts +++ b/project/src/models/eft/common/tables/IBotBase.ts @@ -393,6 +393,8 @@ export interface Productive sptIsComplete?: boolean; /** Is the craft a Continuous, e.g bitcoins/water collector */ sptIsContinuous?: boolean; + /** Stores a list of tools used in this craft, to give back once the craft is done */ + sptRequiredTools?: string[]; } export interface Production extends Productive diff --git a/project/src/models/eft/hideout/IHideoutSingleProductionStartRequestData.ts b/project/src/models/eft/hideout/IHideoutSingleProductionStartRequestData.ts index b63bd356..8e4ff655 100644 --- a/project/src/models/eft/hideout/IHideoutSingleProductionStartRequestData.ts +++ b/project/src/models/eft/hideout/IHideoutSingleProductionStartRequestData.ts @@ -3,6 +3,7 @@ export interface IHideoutSingleProductionStartRequestData Action: "HideoutSingleProductionStart"; recipeId: string; items: Item[]; + tools: Item[]; timestamp: number; }