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 <drakiaxyz@noreply.dev.sp-tarkov.com>
Co-committed-by: DrakiaXYZ <drakiaxyz@noreply.dev.sp-tarkov.com>
This commit is contained in:
DrakiaXYZ 2024-02-25 08:53:57 +00:00 committed by chomp
parent 19013a478f
commit 2adbb6a5fe
4 changed files with 105 additions and 38 deletions

View File

@ -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)
{

View File

@ -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(),
};

View File

@ -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

View File

@ -3,6 +3,7 @@ export interface IHideoutSingleProductionStartRequestData
Action: "HideoutSingleProductionStart";
recipeId: string;
items: Item[];
tools: Item[];
timestamp: number;
}