diff options
Diffstat (limited to 'src/CraftingRecipes.cpp')
-rw-r--r-- | src/CraftingRecipes.cpp | 770 |
1 files changed, 770 insertions, 0 deletions
diff --git a/src/CraftingRecipes.cpp b/src/CraftingRecipes.cpp new file mode 100644 index 000000000..9dc471781 --- /dev/null +++ b/src/CraftingRecipes.cpp @@ -0,0 +1,770 @@ + +// CraftingRecipes.cpp + +// Interfaces to the cCraftingRecipes class representing the storage of crafting recipes + +#include "Globals.h" +#include "CraftingRecipes.h" +#include "Root.h" +#include "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; +} + + + + + +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_ID a_ItemType, int 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) + { + 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++) + { + int idx = x + m_Width * y; + 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_ID a_ItemType, int 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(const cPlayer * a_Player, const 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::auto_ptr<cRecipe> Recipe(FindRecipe(a_CraftingGrid.GetItems(), a_CraftingGrid.GetWidth(), a_CraftingGrid.GetHeight())); + a_Recipe.Clear(); + if (Recipe.get() == NULL) + { + // 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; + f.ReadRestOfFile(Everything); + 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 %d 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) +{ + AStringVector Sides = StringSplit(a_RecipeLine, "="); + if (Sides.size() != 2) + { + LOGWARNING("crafting.txt: line %d: A single '=' was expected, got %d", a_LineNum, (int)Sides.size() - 1); + LOGINFO("Offending line: \"%s\"", a_RecipeLine.c_str()); + return; + } + + std::auto_ptr<cCraftingRecipes::cRecipe> Recipe(new 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) + { + Recipe->m_Result.m_ItemCount = atoi(ResultSplit[1].c_str()); + if (Recipe->m_Result.m_ItemCount == 0) + { + 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]); + a_Item.m_ItemDamage = atoi(Damage.c_str()); + if ((a_Item.m_ItemDamage == 0) && (Damage.compare("0") != 0)) + { + // 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 == NULL) + { + return NULL; + } + + // 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 != NULL) + { + return Recipe; + } + } // for y, for x + } // for itr - m_Recipes[] + + // No matching recipe found + return NULL; +} + + + + + +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); + if ( + (itrS->x >= a_GridWidth) || + (itrS->y >= a_GridHeight) || + (itrS->m_Item.m_ItemType != a_CraftingGrid[GridID].m_ItemType) || // same item type? + (itrS->m_Item.m_ItemCount > a_CraftingGrid[GridID].m_ItemCount) || // not enough items + ( + (itrS->m_Item.m_ItemDamage > 0) && // should compare damage values? + (itrS->m_Item.m_ItemDamage != a_CraftingGrid[GridID].m_ItemDamage) + ) + ) + { + // Doesn't match + return NULL; + } + 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 NULL; + } + } // 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 NULL; + } + } // for y, for x + + // The recipe has matched. Create a copy of the recipe and set its coords to match the crafting grid: + std::auto_ptr<cRecipe> Recipe(new 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.insert(Recipe->m_Ingredients.end(), MatchedSlots.begin(), MatchedSlots.end()); + return Recipe.release(); +} + + + + |