01b8ed5295
The BlockID.h file was removed from Globals.h and renamed to BlockType.h (main change) The BlockInfo.h file was removed from Globals.h (main change) The ENUM_BLOCK_ID and ENUM_ITEM_ID enum names were replaced with ENUM_BLOCK_TYPE and ENUM_ITEM_TYPE (cosmetics) The various enums, such as eDimension, eDamageType and eExplosionSource were moved from BlockType.h to Defines.h, together with the helper functions for converting between them and strings (StringToDimension et al.) (minor) Many inline functions were moved from headers to their respective cpp files, so that BlockType.h could be included only into the cpp file, rather than the header. That broke our tests a bit, since they pick bits and pieces out of the main code and provide stubs for the rest; they had to be re-stubbed and re-verified. eMonsterType values are no longer tied to E_ITEM_SPAWN_EGG_META_* values
1070 lines
27 KiB
C++
1070 lines
27 KiB
C++
|
|
// CraftingRecipes.cpp
|
|
|
|
// Interfaces to the cCraftingRecipes class representing the storage of crafting recipes
|
|
|
|
#include "Globals.h"
|
|
#include "CraftingRecipes.h"
|
|
#include "Root.h"
|
|
#include "Bindings/PluginManager.h"
|
|
|
|
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// cCraftingGrid:
|
|
|
|
cCraftingGrid::cCraftingGrid(int a_Width, int a_Height) :
|
|
m_Width(a_Width),
|
|
m_Height(a_Height),
|
|
m_Items(new cItem[a_Width * a_Height])
|
|
{
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
cCraftingGrid::cCraftingGrid(const cItem * a_Items, int a_Width, int a_Height) :
|
|
m_Width(a_Width),
|
|
m_Height(a_Height),
|
|
m_Items(new cItem[a_Width * a_Height])
|
|
{
|
|
for (int i = a_Width * a_Height - 1; i >= 0; i--)
|
|
{
|
|
m_Items[i] = a_Items[i];
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
cCraftingGrid::cCraftingGrid(const cCraftingGrid & a_Original) :
|
|
m_Width(a_Original.m_Width),
|
|
m_Height(a_Original.m_Height),
|
|
m_Items(new cItem[a_Original.m_Width * a_Original.m_Height])
|
|
{
|
|
for (int i = m_Width * m_Height - 1; i >= 0; i--)
|
|
{
|
|
m_Items[i] = a_Original.m_Items[i];
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
cCraftingGrid::~cCraftingGrid()
|
|
{
|
|
delete[] m_Items;
|
|
m_Items = nullptr;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
cItem & cCraftingGrid::GetItem(int x, int y) const
|
|
{
|
|
// Accessible through scripting, must verify parameters:
|
|
if ((x < 0) || (x >= m_Width) || (y < 0) || (y >= m_Height))
|
|
{
|
|
LOGERROR("Attempted to get an invalid item from a crafting grid: (%d, %d), grid dimensions: (%d, %d).",
|
|
x, y, m_Width, m_Height
|
|
);
|
|
return m_Items[0];
|
|
}
|
|
return m_Items[x + m_Width * y];
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void cCraftingGrid::SetItem(int x, int y, ENUM_ITEM_TYPE a_ItemType, char a_ItemCount, short a_ItemHealth)
|
|
{
|
|
// Accessible through scripting, must verify parameters:
|
|
if ((x < 0) || (x >= m_Width) || (y < 0) || (y >= m_Height))
|
|
{
|
|
LOGERROR("Attempted to set an invalid item in a crafting grid: (%d, %d), grid dimensions: (%d, %d).",
|
|
x, y, m_Width, m_Height
|
|
);
|
|
return;
|
|
}
|
|
|
|
m_Items[x + m_Width * y] = cItem(a_ItemType, a_ItemCount, a_ItemHealth);
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void cCraftingGrid::SetItem(int x, int y, const cItem & a_Item)
|
|
{
|
|
// Accessible through scripting, must verify parameters:
|
|
if ((x < 0) || (x >= m_Width) || (y < 0) || (y >= m_Height))
|
|
{
|
|
LOGERROR("Attempted to set an invalid item in a crafting grid: (%d, %d), grid dimensions: (%d, %d).",
|
|
x, y, m_Width, m_Height
|
|
);
|
|
return;
|
|
}
|
|
|
|
m_Items[x + m_Width * y] = a_Item;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void cCraftingGrid::Clear(void)
|
|
{
|
|
for (int y = 0; y < m_Height; y++) for (int x = 0; x < m_Width; x++)
|
|
{
|
|
m_Items[x + m_Width * y].Empty();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void cCraftingGrid::ConsumeGrid(const cCraftingGrid & a_Grid)
|
|
{
|
|
if ((a_Grid.m_Width != m_Width) || (a_Grid.m_Height != m_Height))
|
|
{
|
|
LOGWARNING("Consuming a grid of different dimensions: (%d, %d) vs (%d, %d)",
|
|
a_Grid.m_Width, a_Grid.m_Height, m_Width, m_Height
|
|
);
|
|
}
|
|
int MinX = std::min(a_Grid.m_Width, m_Width);
|
|
int MinY = std::min(a_Grid.m_Height, m_Height);
|
|
for (int y = 0; y < MinY; y++) for (int x = 0; x < MinX; x++)
|
|
{
|
|
int ThatIdx = x + a_Grid.m_Width * y;
|
|
if (a_Grid.m_Items[ThatIdx].IsEmpty())
|
|
{
|
|
continue;
|
|
}
|
|
int ThisIdx = x + m_Width * y;
|
|
if (a_Grid.m_Items[ThatIdx].m_ItemType != m_Items[ThisIdx].m_ItemType)
|
|
{
|
|
LOGWARNING("Consuming incompatible grids: item at (%d, %d) is %d in grid and %d in ingredients. Item not consumed.",
|
|
x, y, m_Items[ThisIdx].m_ItemType, a_Grid.m_Items[ThatIdx].m_ItemType
|
|
);
|
|
continue;
|
|
}
|
|
char NumWantedItems = a_Grid.m_Items[ThatIdx].m_ItemCount;
|
|
if (NumWantedItems > m_Items[ThisIdx].m_ItemCount)
|
|
{
|
|
LOGWARNING("Consuming more items than there actually are in slot (%d, %d), item %d (want %d, have %d). Item zeroed out.",
|
|
x, y, m_Items[ThisIdx].m_ItemType,
|
|
NumWantedItems, m_Items[ThisIdx].m_ItemCount
|
|
);
|
|
NumWantedItems = m_Items[ThisIdx].m_ItemCount;
|
|
}
|
|
m_Items[ThisIdx].m_ItemCount -= NumWantedItems;
|
|
if (m_Items[ThisIdx].m_ItemCount == 0)
|
|
{
|
|
if ((m_Items[ThisIdx].m_ItemType == E_ITEM_MILK) || (m_Items[ThisIdx].m_ItemType == E_ITEM_WATER_BUCKET) || (m_Items[ThisIdx].m_ItemType == E_ITEM_LAVA_BUCKET))
|
|
{
|
|
m_Items[ThisIdx] = cItem(E_ITEM_BUCKET);
|
|
}
|
|
else
|
|
{
|
|
m_Items[ThisIdx].Clear();
|
|
}
|
|
}
|
|
} // for x, for y
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void cCraftingGrid::CopyToItems(cItem * a_Items) const
|
|
{
|
|
for (int i = m_Height * m_Width - 1; i >= 0; i--)
|
|
{
|
|
a_Items[i] = m_Items[i];
|
|
} // for x, for y
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void cCraftingGrid::Dump(void)
|
|
{
|
|
for (int y = 0; y < m_Height; y++) for (int x = 0; x < m_Width; x++)
|
|
{
|
|
#ifdef _DEBUG
|
|
int idx = x + m_Width * y;
|
|
#endif
|
|
LOGD("Slot (%d, %d): Type %d, health %d, count %d",
|
|
x, y, m_Items[idx].m_ItemType, m_Items[idx].m_ItemDamage, m_Items[idx].m_ItemCount
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// cCraftingRecipe:
|
|
|
|
cCraftingRecipe::cCraftingRecipe(const cCraftingGrid & a_CraftingGrid) :
|
|
m_Ingredients(a_CraftingGrid)
|
|
{
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void cCraftingRecipe::Clear(void)
|
|
{
|
|
m_Ingredients.Clear();
|
|
m_Result.Clear();
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void cCraftingRecipe::SetResult(ENUM_ITEM_TYPE a_ItemType, char a_ItemCount, short a_ItemHealth)
|
|
{
|
|
m_Result = cItem(a_ItemType, a_ItemCount, a_ItemHealth);
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void cCraftingRecipe::ConsumeIngredients(cCraftingGrid & a_CraftingGrid)
|
|
{
|
|
a_CraftingGrid.ConsumeGrid(m_Ingredients);
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void cCraftingRecipe::Dump(void)
|
|
{
|
|
LOGD("Recipe ingredients:");
|
|
m_Ingredients.Dump();
|
|
LOGD("Result: Type %d, health %d, count %d",
|
|
m_Result.m_ItemType, m_Result.m_ItemDamage, m_Result.m_ItemCount
|
|
);
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// cCraftingRecipes:
|
|
|
|
cCraftingRecipes::cCraftingRecipes(void)
|
|
{
|
|
LoadRecipes();
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
cCraftingRecipes::~cCraftingRecipes()
|
|
{
|
|
ClearRecipes();
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void cCraftingRecipes::GetRecipe(cPlayer & a_Player, cCraftingGrid & a_CraftingGrid, cCraftingRecipe & a_Recipe)
|
|
{
|
|
// Allow plugins to intercept recipes using a pre-craft hook:
|
|
if (cRoot::Get()->GetPluginManager()->CallHookPreCrafting(a_Player, a_CraftingGrid, a_Recipe))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Built-in recipes:
|
|
std::unique_ptr<cRecipe> Recipe(FindRecipe(a_CraftingGrid.GetItems(), a_CraftingGrid.GetWidth(), a_CraftingGrid.GetHeight()));
|
|
a_Recipe.Clear();
|
|
if (Recipe.get() == nullptr)
|
|
{
|
|
// Allow plugins to intercept a no-recipe-found situation:
|
|
cRoot::Get()->GetPluginManager()->CallHookCraftingNoRecipe(a_Player, a_CraftingGrid, a_Recipe);
|
|
return;
|
|
}
|
|
for (cRecipeSlots::const_iterator itr = Recipe->m_Ingredients.begin(); itr != Recipe->m_Ingredients.end(); ++itr)
|
|
{
|
|
a_Recipe.SetIngredient(itr->x, itr->y, itr->m_Item);
|
|
} // for itr
|
|
a_Recipe.SetResult(Recipe->m_Result);
|
|
|
|
// Allow plugins to intercept recipes after they are processed:
|
|
cRoot::Get()->GetPluginManager()->CallHookPostCrafting(a_Player, a_CraftingGrid, a_Recipe);
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void cCraftingRecipes::LoadRecipes(void)
|
|
{
|
|
LOGD("Loading crafting recipes from crafting.txt...");
|
|
ClearRecipes();
|
|
|
|
// Load the crafting.txt file:
|
|
cFile f;
|
|
if (!f.Open("crafting.txt", cFile::fmRead))
|
|
{
|
|
LOGWARNING("Cannot open file \"crafting.txt\", no crafting recipes will be available!");
|
|
return;
|
|
}
|
|
AString Everything;
|
|
if (!f.ReadRestOfFile(Everything))
|
|
{
|
|
LOGWARNING("Cannot read file \"crafting.txt\", no crafting recipes will be available!");
|
|
return;
|
|
}
|
|
f.Close();
|
|
|
|
// Split it into lines, then process each line as a single recipe:
|
|
AStringVector Split = StringSplit(Everything, "\n");
|
|
int LineNum = 1;
|
|
for (AStringVector::const_iterator itr = Split.begin(); itr != Split.end(); ++itr, ++LineNum)
|
|
{
|
|
// Remove anything after a '#' sign and trim away the whitespace:
|
|
AString Recipe = TrimString(itr->substr(0, itr->find('#')));
|
|
if (Recipe.empty())
|
|
{
|
|
// Empty recipe
|
|
continue;
|
|
}
|
|
AddRecipeLine(LineNum, Recipe);
|
|
} // for itr - Split[]
|
|
LOG("Loaded %zu crafting recipes", m_Recipes.size());
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void cCraftingRecipes::ClearRecipes(void)
|
|
{
|
|
for (cRecipes::iterator itr = m_Recipes.begin(); itr != m_Recipes.end(); ++itr)
|
|
{
|
|
delete *itr;
|
|
}
|
|
m_Recipes.clear();
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void cCraftingRecipes::AddRecipeLine(int a_LineNum, const AString & a_RecipeLine)
|
|
{
|
|
// Remove any spaces within the line:
|
|
AString RecipeLine(a_RecipeLine);
|
|
RecipeLine.erase(std::remove_if(RecipeLine.begin(), RecipeLine.end(), isspace), RecipeLine.end());
|
|
|
|
AStringVector Sides = StringSplit(RecipeLine, "=");
|
|
if (Sides.size() != 2)
|
|
{
|
|
LOGWARNING("crafting.txt: line %d: A single '=' was expected, got %zu", a_LineNum, Sides.size() - 1);
|
|
LOGINFO("Offending line: \"%s\"", a_RecipeLine.c_str());
|
|
return;
|
|
}
|
|
|
|
std::unique_ptr<cCraftingRecipes::cRecipe> Recipe = cpp14::make_unique<cCraftingRecipes::cRecipe>();
|
|
|
|
// Parse the result:
|
|
AStringVector ResultSplit = StringSplit(Sides[0], ",");
|
|
if (ResultSplit.empty())
|
|
{
|
|
LOGWARNING("crafting.txt: line %d: Result is empty, ignoring the recipe.", a_LineNum);
|
|
LOGINFO("Offending line: \"%s\"", a_RecipeLine.c_str());
|
|
return;
|
|
}
|
|
if (!ParseItem(ResultSplit[0], Recipe->m_Result))
|
|
{
|
|
LOGWARNING("crafting.txt: line %d: Cannot parse result item, ignoring the recipe.", a_LineNum);
|
|
LOGINFO("Offending line: \"%s\"", a_RecipeLine.c_str());
|
|
return;
|
|
}
|
|
if (ResultSplit.size() > 1)
|
|
{
|
|
if (!StringToInteger<char>(ResultSplit[1].c_str(), Recipe->m_Result.m_ItemCount))
|
|
{
|
|
LOGWARNING("crafting.txt: line %d: Cannot parse result count, ignoring the recipe.", a_LineNum);
|
|
LOGINFO("Offending line: \"%s\"", a_RecipeLine.c_str());
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Recipe->m_Result.m_ItemCount = 1;
|
|
}
|
|
|
|
// Parse each ingredient:
|
|
AStringVector Ingredients = StringSplit(Sides[1], "|");
|
|
int Num = 1;
|
|
for (AStringVector::const_iterator itr = Ingredients.begin(); itr != Ingredients.end(); ++itr, ++Num)
|
|
{
|
|
if (!ParseIngredient(*itr, Recipe.get()))
|
|
{
|
|
LOGWARNING("crafting.txt: line %d: Cannot parse ingredient #%d, ignoring the recipe.", a_LineNum, Num);
|
|
LOGINFO("Offending line: \"%s\"", a_RecipeLine.c_str());
|
|
return;
|
|
}
|
|
} // for itr - Ingredients[]
|
|
|
|
NormalizeIngredients(Recipe.get());
|
|
|
|
m_Recipes.push_back(Recipe.release());
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool cCraftingRecipes::ParseItem(const AString & a_String, cItem & a_Item)
|
|
{
|
|
// The caller provides error logging
|
|
|
|
AStringVector Split = StringSplit(a_String, "^");
|
|
if (Split.empty())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!StringToItem(Split[0], a_Item))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (Split.size() > 1)
|
|
{
|
|
AString Damage = TrimString(Split[1]);
|
|
if (!StringToInteger<short>(Damage.c_str(), a_Item.m_ItemDamage))
|
|
{
|
|
// Parsing the number failed
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Success
|
|
return true;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool cCraftingRecipes::ParseIngredient(const AString & a_String, cRecipe * a_Recipe)
|
|
{
|
|
// a_String is in this format: "ItemType^damage, X:Y, X:Y, X:Y..."
|
|
AStringVector Split = StringSplit(a_String, ",");
|
|
if (Split.size() < 2)
|
|
{
|
|
// Not enough split items
|
|
return false;
|
|
}
|
|
cItem Item;
|
|
if (!ParseItem(Split[0], Item))
|
|
{
|
|
return false;
|
|
}
|
|
Item.m_ItemCount = 1;
|
|
|
|
cCraftingRecipes::cRecipeSlots TempSlots;
|
|
for (AStringVector::const_iterator itr = Split.begin() + 1; itr != Split.end(); ++itr)
|
|
{
|
|
// Parse the coords in the split item:
|
|
AStringVector Coords = StringSplit(*itr, ":");
|
|
if ((Coords.size() == 1) && (TrimString(Coords[0]) == "*"))
|
|
{
|
|
cCraftingRecipes::cRecipeSlot Slot;
|
|
Slot.m_Item = Item;
|
|
Slot.x = -1;
|
|
Slot.y = -1;
|
|
TempSlots.push_back(Slot);
|
|
continue;
|
|
}
|
|
if (Coords.size() != 2)
|
|
{
|
|
return false;
|
|
}
|
|
Coords[0] = TrimString(Coords[0]);
|
|
Coords[1] = TrimString(Coords[1]);
|
|
if (Coords[0].empty() || Coords[1].empty())
|
|
{
|
|
return false;
|
|
}
|
|
cCraftingRecipes::cRecipeSlot Slot;
|
|
Slot.m_Item = Item;
|
|
switch (Coords[0][0])
|
|
{
|
|
case '1': Slot.x = 0; break;
|
|
case '2': Slot.x = 1; break;
|
|
case '3': Slot.x = 2; break;
|
|
case '*': Slot.x = -1; break;
|
|
default:
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
switch (Coords[1][0])
|
|
{
|
|
case '1': Slot.y = 0; break;
|
|
case '2': Slot.y = 1; break;
|
|
case '3': Slot.y = 2; break;
|
|
case '*': Slot.y = -1; break;
|
|
default:
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
TempSlots.push_back(Slot);
|
|
} // for itr - Split[]
|
|
|
|
// Append the ingredients:
|
|
a_Recipe->m_Ingredients.insert(a_Recipe->m_Ingredients.end(), TempSlots.begin(), TempSlots.end());
|
|
return true;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void cCraftingRecipes::NormalizeIngredients(cCraftingRecipes::cRecipe * a_Recipe)
|
|
{
|
|
// Calculate the minimum coords for ingredients, excluding the "anywhere" items:
|
|
int MinX = MAX_GRID_WIDTH, MaxX = 0;
|
|
int MinY = MAX_GRID_HEIGHT, MaxY = 0;
|
|
for (cRecipeSlots::const_iterator itr = a_Recipe->m_Ingredients.begin(); itr != a_Recipe->m_Ingredients.end(); ++itr)
|
|
{
|
|
if (itr->x >= 0)
|
|
{
|
|
MinX = std::min(itr->x, MinX);
|
|
MaxX = std::max(itr->x, MaxX);
|
|
}
|
|
if (itr->y >= 0)
|
|
{
|
|
MinY = std::min(itr->y, MinY);
|
|
MaxY = std::max(itr->y, MaxY);
|
|
}
|
|
} // for itr - a_Recipe->m_Ingredients[]
|
|
|
|
// Move ingredients so that the minimum coords are 0:0
|
|
for (cRecipeSlots::iterator itr = a_Recipe->m_Ingredients.begin(); itr != a_Recipe->m_Ingredients.end(); ++itr)
|
|
{
|
|
if (itr->x >= 0)
|
|
{
|
|
itr->x -= MinX;
|
|
}
|
|
if (itr->y >= 0)
|
|
{
|
|
itr->y -= MinY;
|
|
}
|
|
} // for itr - a_Recipe->m_Ingredients[]
|
|
a_Recipe->m_Width = std::max(MaxX - MinX + 1, 1);
|
|
a_Recipe->m_Height = std::max(MaxY - MinY + 1, 1);
|
|
|
|
// TODO: Compress two same ingredients with the same coords into a single ingredient with increased item count
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
cCraftingRecipes::cRecipe * cCraftingRecipes::FindRecipe(const cItem * a_CraftingGrid, int a_GridWidth, int a_GridHeight)
|
|
{
|
|
ASSERT(a_GridWidth <= MAX_GRID_WIDTH);
|
|
ASSERT(a_GridHeight <= MAX_GRID_HEIGHT);
|
|
|
|
// Get the real bounds of the crafting grid:
|
|
int GridLeft = MAX_GRID_WIDTH, GridTop = MAX_GRID_HEIGHT;
|
|
int GridRight = 0, GridBottom = 0;
|
|
for (int y = 0; y < a_GridHeight; y++) for (int x = 0; x < a_GridWidth; x++)
|
|
{
|
|
if (!a_CraftingGrid[x + y * a_GridWidth].IsEmpty())
|
|
{
|
|
GridRight = std::max(x, GridRight);
|
|
GridBottom = std::max(y, GridBottom);
|
|
GridLeft = std::min(x, GridLeft);
|
|
GridTop = std::min(y, GridTop);
|
|
}
|
|
}
|
|
int GridWidth = GridRight - GridLeft + 1;
|
|
int GridHeight = GridBottom - GridTop + 1;
|
|
|
|
// Search in the possibly minimized grid, but keep the stride:
|
|
const cItem * Grid = a_CraftingGrid + GridLeft + (a_GridWidth * GridTop);
|
|
cRecipe * Recipe = FindRecipeCropped(Grid, GridWidth, GridHeight, a_GridWidth);
|
|
if (Recipe == nullptr)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
// A recipe has been found, move it to correspond to the original crafting grid:
|
|
for (cRecipeSlots::iterator itrS = Recipe->m_Ingredients.begin(); itrS != Recipe->m_Ingredients.end(); ++itrS)
|
|
{
|
|
itrS->x += GridLeft;
|
|
itrS->y += GridTop;
|
|
} // for itrS - Recipe->m_Ingredients[]
|
|
|
|
return Recipe;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
cCraftingRecipes::cRecipe * cCraftingRecipes::FindRecipeCropped(const cItem * a_CraftingGrid, int a_GridWidth, int a_GridHeight, int a_GridStride)
|
|
{
|
|
for (cRecipes::const_iterator itr = m_Recipes.begin(); itr != m_Recipes.end(); ++itr)
|
|
{
|
|
// Both the crafting grid and the recipes are normalized. The only variable possible is the "anywhere" items.
|
|
// This still means that the "anywhere" item may be the one that is offsetting the grid contents to the right or downwards, so we need to check all possible positions.
|
|
// E. g. recipe "A, * | B, 1:1 | ..." still needs to check grid for B at 2:2 (in case A was in grid's 1:1)
|
|
// Calculate the maximum offsets for this recipe relative to the grid size, and iterate through all combinations of offsets.
|
|
// Also, this calculation automatically filters out recipes that are too large for the current grid - the loop won't be entered at all.
|
|
|
|
int MaxOfsX = a_GridWidth - (*itr)->m_Width;
|
|
int MaxOfsY = a_GridHeight - (*itr)->m_Height;
|
|
for (int x = 0; x <= MaxOfsX; x++) for (int y = 0; y <= MaxOfsY; y++)
|
|
{
|
|
cRecipe * Recipe = MatchRecipe(a_CraftingGrid, a_GridWidth, a_GridHeight, a_GridStride, *itr, x, y);
|
|
if (Recipe != nullptr)
|
|
{
|
|
return Recipe;
|
|
}
|
|
} // for y, for x
|
|
} // for itr - m_Recipes[]
|
|
|
|
// No matching recipe found
|
|
return nullptr;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
cCraftingRecipes::cRecipe * cCraftingRecipes::MatchRecipe(const cItem * a_CraftingGrid, int a_GridWidth, int a_GridHeight, int a_GridStride, const cRecipe * a_Recipe, int a_OffsetX, int a_OffsetY)
|
|
{
|
|
// Check the regular items first:
|
|
bool HasMatched[MAX_GRID_WIDTH][MAX_GRID_HEIGHT];
|
|
memset(HasMatched, 0, sizeof(HasMatched));
|
|
for (cRecipeSlots::const_iterator itrS = a_Recipe->m_Ingredients.begin(); itrS != a_Recipe->m_Ingredients.end(); ++itrS)
|
|
{
|
|
if ((itrS->x < 0) || (itrS->y < 0))
|
|
{
|
|
// "Anywhere" item, process later
|
|
continue;
|
|
}
|
|
ASSERT(itrS->x + a_OffsetX < a_GridWidth);
|
|
ASSERT(itrS->y + a_OffsetY < a_GridHeight);
|
|
int GridID = (itrS->x + a_OffsetX) + a_GridStride * (itrS->y + a_OffsetY);
|
|
|
|
const cItem & Item = itrS->m_Item;
|
|
if (
|
|
(itrS->x >= a_GridWidth) ||
|
|
(itrS->y >= a_GridHeight) ||
|
|
(Item.m_ItemType != a_CraftingGrid[GridID].m_ItemType) || // same item type?
|
|
(Item.m_ItemCount > a_CraftingGrid[GridID].m_ItemCount) || // not enough items
|
|
(
|
|
(Item.m_ItemDamage >= 0) && // should compare damage values?
|
|
(Item.m_ItemDamage != a_CraftingGrid[GridID].m_ItemDamage)
|
|
)
|
|
)
|
|
{
|
|
// Doesn't match
|
|
return nullptr;
|
|
}
|
|
HasMatched[itrS->x + a_OffsetX][itrS->y + a_OffsetY] = true;
|
|
} // for itrS - Recipe->m_Ingredients[]
|
|
|
|
// Process the "Anywhere" items now, and only in the cells that haven't matched yet
|
|
// The "anywhere" items are processed on a first-come-first-served basis.
|
|
// Do not use a recipe with one horizontal and one vertical "anywhere" ("*:1, 1:*") as it may not match properly!
|
|
cRecipeSlots MatchedSlots; // Stores the slots of "anywhere" items that have matched, with the match coords
|
|
for (cRecipeSlots::const_iterator itrS = a_Recipe->m_Ingredients.begin(); itrS != a_Recipe->m_Ingredients.end(); ++itrS)
|
|
{
|
|
if ((itrS->x >= 0) && (itrS->y >= 0))
|
|
{
|
|
// Regular item, already processed
|
|
continue;
|
|
}
|
|
int StartX = 0, EndX = a_GridWidth - 1;
|
|
int StartY = 0, EndY = a_GridHeight - 1;
|
|
if (itrS->x >= 0)
|
|
{
|
|
StartX = itrS->x;
|
|
EndX = itrS->x;
|
|
}
|
|
else if (itrS->y >= 0)
|
|
{
|
|
StartY = itrS->y;
|
|
EndY = itrS->y;
|
|
}
|
|
bool Found = false;
|
|
for (int x = StartX; x <= EndX; x++)
|
|
{
|
|
for (int y = StartY; y <= EndY; y++)
|
|
{
|
|
if (HasMatched[x][y])
|
|
{
|
|
// Already matched some other item
|
|
continue;
|
|
}
|
|
int GridIdx = x + a_GridStride * y;
|
|
if (
|
|
(a_CraftingGrid[GridIdx].m_ItemType == itrS->m_Item.m_ItemType) &&
|
|
(
|
|
(itrS->m_Item.m_ItemDamage < 0) || // doesn't want damage comparison
|
|
(itrS->m_Item.m_ItemDamage == a_CraftingGrid[GridIdx].m_ItemDamage) // the damage matches
|
|
)
|
|
)
|
|
{
|
|
HasMatched[x][y] = true;
|
|
Found = true;
|
|
MatchedSlots.push_back(*itrS);
|
|
MatchedSlots.back().x = x;
|
|
MatchedSlots.back().y = y;
|
|
break;
|
|
}
|
|
} // for y
|
|
if (Found)
|
|
{
|
|
break;
|
|
}
|
|
} // for x
|
|
if (!Found)
|
|
{
|
|
return nullptr;
|
|
}
|
|
} // for itrS - a_Recipe->m_Ingredients[]
|
|
|
|
// Check if the whole grid has matched:
|
|
for (int x = 0; x < a_GridWidth; x++) for (int y = 0; y < a_GridHeight; y++)
|
|
{
|
|
if (!HasMatched[x][y] && !a_CraftingGrid[x + a_GridStride * y].IsEmpty())
|
|
{
|
|
// There's an unmatched item in the grid
|
|
return nullptr;
|
|
}
|
|
} // for y, for x
|
|
|
|
// The recipe has matched. Create a copy of the recipe and set its coords to match the crafting grid:
|
|
std::unique_ptr<cRecipe> Recipe = cpp14::make_unique<cRecipe>();
|
|
Recipe->m_Result = a_Recipe->m_Result;
|
|
Recipe->m_Width = a_Recipe->m_Width;
|
|
Recipe->m_Height = a_Recipe->m_Height;
|
|
for (cRecipeSlots::const_iterator itrS = a_Recipe->m_Ingredients.begin(); itrS != a_Recipe->m_Ingredients.end(); ++itrS)
|
|
{
|
|
if ((itrS->x < 0) || (itrS->y < 0))
|
|
{
|
|
// "Anywhere" item, process later
|
|
continue;
|
|
}
|
|
Recipe->m_Ingredients.push_back(*itrS);
|
|
Recipe->m_Ingredients.back().x += a_OffsetX;
|
|
Recipe->m_Ingredients.back().y += a_OffsetY;
|
|
}
|
|
Recipe->m_Ingredients.insert(Recipe->m_Ingredients.end(), MatchedSlots.begin(), MatchedSlots.end());
|
|
|
|
// Handle the fireworks-related effects:
|
|
// We use Recipe instead of a_Recipe because we want the wildcard ingredients' slot numbers as well, which was just added previously
|
|
HandleFireworks(a_CraftingGrid, Recipe.get(), a_GridStride, a_OffsetX, a_OffsetY);
|
|
|
|
// Handle Dyed Leather
|
|
HandleDyedLeather(a_CraftingGrid, Recipe.get(), a_GridStride, a_GridWidth, a_GridHeight);
|
|
|
|
return Recipe.release();
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void cCraftingRecipes::HandleFireworks(const cItem * a_CraftingGrid, cCraftingRecipes::cRecipe * a_Recipe, int a_GridStride, int a_OffsetX, int a_OffsetY)
|
|
{
|
|
// TODO: add support for more than one dye in the recipe
|
|
// A manual and temporary solution (listing everything) is in crafting.txt for fade colours, but a programmatic solutions needs to be done for everything else
|
|
|
|
if (a_Recipe->m_Result.m_ItemType == E_ITEM_FIREWORK_ROCKET)
|
|
{
|
|
for (cRecipeSlots::const_iterator itr = a_Recipe->m_Ingredients.begin(); itr != a_Recipe->m_Ingredients.end(); ++itr)
|
|
{
|
|
switch (itr->m_Item.m_ItemType)
|
|
{
|
|
case E_ITEM_FIREWORK_STAR:
|
|
{
|
|
// Result was a rocket, found a star - copy star data to rocket data
|
|
int GridID = (itr->x + a_OffsetX) + a_GridStride * (itr->y + a_OffsetY);
|
|
a_Recipe->m_Result.m_FireworkItem.CopyFrom(a_CraftingGrid[GridID].m_FireworkItem);
|
|
break;
|
|
}
|
|
case E_ITEM_GUNPOWDER:
|
|
{
|
|
// Gunpowder - increase flight time
|
|
a_Recipe->m_Result.m_FireworkItem.m_FlightTimeInTicks += 20;
|
|
break;
|
|
}
|
|
case E_ITEM_PAPER: break;
|
|
default: LOG("Unexpected item in firework rocket recipe, was the crafting file's fireworks section changed?"); break;
|
|
}
|
|
}
|
|
}
|
|
else if (a_Recipe->m_Result.m_ItemType == E_ITEM_FIREWORK_STAR)
|
|
{
|
|
std::vector<int> DyeColours;
|
|
bool FoundStar = false;
|
|
|
|
for (cRecipeSlots::const_iterator itr = a_Recipe->m_Ingredients.begin(); itr != a_Recipe->m_Ingredients.end(); ++itr)
|
|
{
|
|
switch (itr->m_Item.m_ItemType)
|
|
{
|
|
case E_ITEM_FIREWORK_STAR:
|
|
{
|
|
// Result was star, found another star - probably adding fade colours, but copy data over anyhow
|
|
FoundStar = true;
|
|
int GridID = (itr->x + a_OffsetX) + a_GridStride * (itr->y + a_OffsetY);
|
|
a_Recipe->m_Result.m_FireworkItem.CopyFrom(a_CraftingGrid[GridID].m_FireworkItem);
|
|
break;
|
|
}
|
|
case E_ITEM_DYE:
|
|
{
|
|
int GridID = (itr->x + a_OffsetX) + a_GridStride * (itr->y + a_OffsetY);
|
|
DyeColours.push_back(cFireworkItem::GetVanillaColourCodeFromDye(static_cast<NIBBLETYPE>(a_CraftingGrid[GridID].m_ItemDamage & 0x0f)));
|
|
break;
|
|
}
|
|
case E_ITEM_GUNPOWDER: break;
|
|
case E_ITEM_DIAMOND: a_Recipe->m_Result.m_FireworkItem.m_HasTrail = true; break;
|
|
case E_ITEM_GLOWSTONE_DUST: a_Recipe->m_Result.m_FireworkItem.m_HasFlicker = true; break;
|
|
|
|
case E_ITEM_FIRE_CHARGE: a_Recipe->m_Result.m_FireworkItem.m_Type = 1; break;
|
|
case E_ITEM_GOLD_NUGGET: a_Recipe->m_Result.m_FireworkItem.m_Type = 2; break;
|
|
case E_ITEM_FEATHER: a_Recipe->m_Result.m_FireworkItem.m_Type = 4; break;
|
|
case E_ITEM_HEAD: a_Recipe->m_Result.m_FireworkItem.m_Type = 3; break;
|
|
default: LOG("Unexpected item in firework star recipe, was the crafting file's fireworks section changed?"); break; // ermahgerd BARD ardmins
|
|
}
|
|
}
|
|
|
|
if (FoundStar && (!DyeColours.empty()))
|
|
{
|
|
// Found a star and a dye? Fade colours.
|
|
a_Recipe->m_Result.m_FireworkItem.m_FadeColours = DyeColours;
|
|
}
|
|
else if (!DyeColours.empty())
|
|
{
|
|
// Only dye? Normal colours.
|
|
a_Recipe->m_Result.m_FireworkItem.m_Colours = DyeColours;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void cCraftingRecipes::HandleDyedLeather(const cItem * a_CraftingGrid, cCraftingRecipes::cRecipe * a_Recipe, int a_GridStride, int a_GridWidth, int a_GridHeight)
|
|
{
|
|
short result_type = a_Recipe->m_Result.m_ItemType;
|
|
if ((result_type == E_ITEM_LEATHER_CAP) || (result_type == E_ITEM_LEATHER_TUNIC) || (result_type == E_ITEM_LEATHER_PANTS) || (result_type == E_ITEM_LEATHER_BOOTS))
|
|
{
|
|
bool found = false;
|
|
cItem temp;
|
|
|
|
float red = 0;
|
|
float green = 0;
|
|
float blue = 0;
|
|
float dye_count = 0;
|
|
|
|
for (int x = 0; x < a_GridWidth; ++x)
|
|
{
|
|
for (int y = 0; y < a_GridHeight; ++y)
|
|
{
|
|
int GridIdx = x + a_GridStride * y;
|
|
if ((a_CraftingGrid[GridIdx].m_ItemType == result_type) && (found == false))
|
|
{
|
|
found = true;
|
|
temp = a_CraftingGrid[GridIdx].CopyOne();
|
|
// The original color of the item affects the result
|
|
if (temp.m_ItemColor.IsValid())
|
|
{
|
|
red += temp.m_ItemColor.GetRed();
|
|
green += temp.m_ItemColor.GetGreen();
|
|
blue += temp.m_ItemColor.GetBlue();
|
|
++dye_count;
|
|
}
|
|
}
|
|
else if (a_CraftingGrid[GridIdx].m_ItemType == E_ITEM_DYE)
|
|
{
|
|
switch (a_CraftingGrid[GridIdx].m_ItemDamage)
|
|
{
|
|
case E_META_DYE_BLACK:
|
|
{
|
|
red += 23;
|
|
green += 23;
|
|
blue += 23;
|
|
break;
|
|
}
|
|
case E_META_DYE_RED:
|
|
{
|
|
red += 142;
|
|
green += 47;
|
|
blue += 47;
|
|
break;
|
|
}
|
|
case E_META_DYE_GREEN:
|
|
{
|
|
red += 95;
|
|
green += 118;
|
|
blue += 47;
|
|
break;
|
|
}
|
|
case E_META_DYE_BROWN:
|
|
{
|
|
red += 95;
|
|
green += 71;
|
|
blue += 47;
|
|
break;
|
|
}
|
|
case E_META_DYE_BLUE:
|
|
{
|
|
red += 47;
|
|
green += 71;
|
|
blue += 165;
|
|
break;
|
|
}
|
|
case E_META_DYE_PURPLE:
|
|
{
|
|
red += 118;
|
|
green += 59;
|
|
blue += 165;
|
|
break;
|
|
}
|
|
case E_META_DYE_CYAN:
|
|
{
|
|
red += 71;
|
|
green += 118;
|
|
blue += 142;
|
|
break;
|
|
}
|
|
case E_META_DYE_LIGHTGRAY:
|
|
{
|
|
red += 142;
|
|
green += 142;
|
|
blue += 142;
|
|
break;
|
|
}
|
|
case E_META_DYE_GRAY:
|
|
{
|
|
red += 71;
|
|
green += 71;
|
|
blue += 71;
|
|
break;
|
|
}
|
|
case E_META_DYE_PINK:
|
|
{
|
|
red += 225;
|
|
green += 118;
|
|
blue += 153;
|
|
break;
|
|
}
|
|
case E_META_DYE_LIGHTGREEN:
|
|
{
|
|
red += 118;
|
|
green += 190;
|
|
blue += 23;
|
|
break;
|
|
}
|
|
case E_META_DYE_YELLOW:
|
|
{
|
|
red += 213;
|
|
green += 213;
|
|
blue += 47;
|
|
break;
|
|
}
|
|
case E_META_DYE_LIGHTBLUE:
|
|
{
|
|
red += 95;
|
|
green += 142;
|
|
blue += 201;
|
|
break;
|
|
}
|
|
case E_META_DYE_MAGENTA:
|
|
{
|
|
red += 165;
|
|
green += 71;
|
|
blue += 201;
|
|
break;
|
|
}
|
|
case E_META_DYE_ORANGE:
|
|
{
|
|
red += 201;
|
|
green += 118;
|
|
blue += 47;
|
|
break;
|
|
}
|
|
case E_META_DYE_WHITE:
|
|
{
|
|
red += 237;
|
|
green += 237;
|
|
blue += 237;
|
|
break;
|
|
}
|
|
}
|
|
++dye_count;
|
|
}
|
|
else if (a_CraftingGrid[GridIdx].m_ItemType != E_ITEM_EMPTY)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!found)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Calculate the rgb values
|
|
double maximum = static_cast<double>(std::max({red, green, blue}));
|
|
|
|
double average_red = red / dye_count;
|
|
double average_green = green / dye_count;
|
|
double average_blue = blue / dye_count;
|
|
double average_max = maximum / dye_count;
|
|
|
|
double max_average = std::max({average_red, average_green, average_blue});
|
|
|
|
double gain_factor = average_max / max_average;
|
|
|
|
|
|
unsigned char result_red = static_cast<unsigned char>(average_red * gain_factor);
|
|
unsigned char result_green = static_cast<unsigned char>(average_green * gain_factor);
|
|
unsigned char result_blue = static_cast<unsigned char>(average_blue * gain_factor);
|
|
|
|
// Set the results values
|
|
a_Recipe->m_Result = temp;
|
|
a_Recipe->m_Result.m_ItemColor.SetColor(result_red, result_green, result_blue);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|