From 36eab1b3237dbeeaaf5b48808bf0d47eb4bd32e9 Mon Sep 17 00:00:00 2001 From: Tobias Wilken Date: Tue, 14 Jul 2020 18:56:42 +0200 Subject: Introduce recipe book functionality (#4493) * Introduce recipe book functionality The recipe book helps especially new players. Missing it gives the impression that cuberite is not as advanced as it is. The handling of the recipe book uses the following functions: - Unlock Recipes (https://wiki.vg/index.php?title=Protocol&oldid=14204#Unlock_Recipes) to make recipes available and show the notification for new recipes. Initialization is done on player login for known ones, the update is done when new items are discovered. - Craft Recipe Request (https://wiki.vg/index.php?title=Protocol&oldid=14204#Craft_Recipe_Request) when the user selects a recipe from the recipe book to fill the slots. Known recipes are initialized on player login via `Unlock Recipes` with `Action` 0. As soon as a new recipe is discovered this is added via `Unlock Recipes` with `Action` 1. To be able to know and recognize new recipes the player class is extended with `KnownItems` and `KnownRecipes`. As soon as a player touches an item this is compared to the list of `KnownItems`, if the item is unknown the recipes are checked for this item and the other ingredients are checked with the list of `KnownItems`. If a full match is discovered the recipe is unlocked with the client and stored in the `KnownRecipes`. To unlock recipes the recipe ID is sent to the client. A mapping file (for protocol 1.12.2) translated the minecraft recipe names to ids. The crafting.txt is extended with and minecraft recipe names is possible. Limitations: Only a single recipe is added to the crafting area. Multiple clicks or shift click does not increase the number of builds. Co-authored-by: peterbell10 * Address first issues mentioned by @peterbell10 - Some linting - Extract loading of recipe specific protocol mapping into a function - Build `RecipeNameMap` only once - Use `std::optional` - Extract `LoadRecipe` from `Window` * Start to implement new suggestions * Update with suggestions from @peterbell10 * Some minor cleanup * Update protocol packet IDs * Remove unused include * Include header in cmake * Change a vector to integer counter * Change dromedaryCase method names to PascalCase * Address suggestions from @madmaxoft * Read Protocol subdirectories to load recipe books To load all recipebooks iterate over the `Protocol` subdirectories to find mapping files. Co-authored-by: peterbell10 --- src/ClientHandle.cpp | 44 ++++++++++++- src/ClientHandle.h | 10 +++ src/CraftingRecipes.cpp | 95 ++++++++++++++++++++++++-- src/CraftingRecipes.h | 52 ++++++++++++--- src/Entities/Player.cpp | 102 +++++++++++++++++++++++++--- src/Entities/Player.h | 14 ++++ src/Inventory.cpp | 26 ++++++-- src/Inventory.h | 7 +- src/Item.h | 25 +++++++ src/ItemGrid.cpp | 29 ++++++-- src/ItemGrid.h | 6 +- src/Protocol/CMakeLists.txt | 2 + src/Protocol/Packetizer.cpp | 5 +- src/Protocol/Protocol.h | 3 + src/Protocol/ProtocolRecognizer.cpp | 21 ++++++ src/Protocol/ProtocolRecognizer.h | 2 + src/Protocol/Protocol_1_12.cpp | 76 ++++++++++++++++++++- src/Protocol/Protocol_1_12.h | 9 ++- src/Protocol/Protocol_1_13.cpp | 1 + src/Protocol/Protocol_1_8.cpp | 20 ++++++ src/Protocol/Protocol_1_8.h | 2 + src/Protocol/Protocol_1_9.cpp | 6 ++ src/Protocol/RecipeMapper.cpp | 128 ++++++++++++++++++++++++++++++++++++ src/Protocol/RecipeMapper.h | 35 ++++++++++ src/Root.cpp | 2 + src/Root.h | 8 +-- src/UI/CraftingWindow.cpp | 6 ++ src/UI/CraftingWindow.h | 7 +- src/UI/InventoryWindow.cpp | 6 ++ src/UI/InventoryWindow.h | 6 +- src/UI/SlotArea.cpp | 70 ++++++++++++++++++-- src/UI/SlotArea.h | 9 +-- 32 files changed, 761 insertions(+), 73 deletions(-) create mode 100644 src/Protocol/RecipeMapper.cpp create mode 100644 src/Protocol/RecipeMapper.h (limited to 'src') diff --git a/src/ClientHandle.cpp b/src/ClientHandle.cpp index 3cad17849..9fbc9f89d 100644 --- a/src/ClientHandle.cpp +++ b/src/ClientHandle.cpp @@ -14,6 +14,8 @@ #include "BlockEntities/ChestEntity.h" #include "BlockEntities/CommandBlockEntity.h" #include "BlockEntities/SignEntity.h" +#include "UI/InventoryWindow.h" +#include "UI/CraftingWindow.h" #include "UI/Window.h" #include "UI/AnvilWindow.h" #include "UI/BeaconWindow.h" @@ -1692,7 +1694,7 @@ void cClientHandle::HandleWindowClick(UInt8 a_WindowID, Int16 a_SlotNum, eClickA LOGWARNING("Player \"%s\" clicked in a non-existent window. Ignoring", m_Username.c_str()); return; } - + m_Player->AddKnownItem(a_HeldItem); Window->Clicked(*m_Player, a_WindowID, a_SlotNum, a_ClickAction, a_HeldItem); } @@ -3129,6 +3131,46 @@ void cClientHandle::SendUseBed(const cEntity & a_Entity, int a_BlockX, int a_Blo +void cClientHandle::SendUnlockRecipe(UInt32 a_RecipeId) +{ + m_Protocol->SendUnlockRecipe(a_RecipeId); +} + + + + + +void cClientHandle::SendInitRecipes(UInt32 a_RecipeId) +{ + m_Protocol->SendInitRecipes(a_RecipeId); +} + + + + + +void cClientHandle::HandleCraftRecipe(UInt32 a_RecipeId) +{ + auto * Window = m_Player->GetWindow(); + if (Window == nullptr) + { + return; + } + + if (Window->GetWindowType() == cWindow::wtInventory) + { + static_cast(Window)->LoadRecipe(*m_Player, a_RecipeId); + } + else if (Window->GetWindowType() == cWindow::wtWorkbench) + { + static_cast(Window)->LoadRecipe(*m_Player, a_RecipeId); + } +} + + + + + void cClientHandle::SendWeather(eWeather a_Weather) { m_Protocol->SendWeather(a_Weather); diff --git a/src/ClientHandle.h b/src/ClientHandle.h index 8495fb239..1d988b137 100644 --- a/src/ClientHandle.h +++ b/src/ClientHandle.h @@ -221,6 +221,13 @@ public: // tolua_export void SendUpdateBlockEntity (cBlockEntity & a_BlockEntity); void SendUpdateSign (int a_BlockX, int a_BlockY, int a_BlockZ, const AString & a_Line1, const AString & a_Line2, const AString & a_Line3, const AString & a_Line4); void SendUseBed (const cEntity & a_Entity, int a_BlockX, int a_BlockY, int a_BlockZ); + + /** Send a newly discovered recipe to show the notification and unlock in the recipe book */ + void SendUnlockRecipe (UInt32 a_RecipeId); + + /** Send already known recipes without notification but visible in the recipe book */ + void SendInitRecipes (UInt32 a_RecipeId); + void SendWeather (eWeather a_Weather); void SendWholeInventory (const cWindow & a_Window); void SendWindowClose (const cWindow & a_Window); @@ -371,6 +378,9 @@ public: // tolua_export void HandleWindowClick (UInt8 a_WindowID, Int16 a_SlotNum, eClickAction a_ClickAction, const cItem & a_HeldItem); void HandleWindowClose (UInt8 a_WindowID); + /** Called when a recipe from the recipe book is selected */ + void HandleCraftRecipe (UInt32 a_RecipeId); + /** Called when the protocol has finished logging the user in. Return true to allow the user in; false to kick them. */ diff --git a/src/CraftingRecipes.cpp b/src/CraftingRecipes.cpp index bdc98e151..7cd41ec97 100644 --- a/src/CraftingRecipes.cpp +++ b/src/CraftingRecipes.cpp @@ -268,6 +268,7 @@ void cCraftingRecipe::Dump(void) cCraftingRecipes::cCraftingRecipes(void) { LoadRecipes(); + PopulateRecipeNameMap(); } @@ -283,6 +284,72 @@ cCraftingRecipes::~cCraftingRecipes() +bool cCraftingRecipes::IsNewCraftableRecipe(const cRecipe * a_Recipe, const cItem & a_Item, const std::set & a_KnownItems) +{ + bool ContainsNewItem = false; + for (const auto & Ingredient : a_Recipe->m_Ingredients) + { + if ( + (Ingredient.m_Item.m_ItemType == a_Item.m_ItemType) && + ( + (Ingredient.m_Item.m_ItemDamage == a_Item.m_ItemDamage) || + (Ingredient.m_Item.m_ItemDamage == -1) + ) + ) + { + ContainsNewItem = true; + } + if (a_KnownItems.find(Ingredient.m_Item) == a_KnownItems.end()) + { + return false; + } + } + return ContainsNewItem; +} + + + + + +std::vector cCraftingRecipes::FindNewRecipesForItem(const cItem & a_Item, const std::set & a_KnownItems) +{ + std::vector Recipes; + for (UInt32 i = 0; i < m_Recipes.size(); i++) + { + if (m_Recipes[i]->m_RecipeName.empty()) + { + continue; + } + if (IsNewCraftableRecipe(m_Recipes[i], a_Item, a_KnownItems)) + { + Recipes.push_back(i); + } + } + return Recipes; +} + + + + + +const std::map & cCraftingRecipes::GetRecipeNameMap() +{ + return m_RecipeNameMap; +} + + + + + +cCraftingRecipes::cRecipe * cCraftingRecipes::GetRecipeById(UInt32 a_RecipeId) +{ + return m_Recipes[a_RecipeId]; +} + + + + + void cCraftingRecipes::GetRecipe(cPlayer & a_Player, cCraftingGrid & a_CraftingGrid, cCraftingRecipe & a_Recipe) { // Allow plugins to intercept recipes using a pre-craft hook: @@ -355,6 +422,21 @@ void cCraftingRecipes::LoadRecipes(void) +void cCraftingRecipes::PopulateRecipeNameMap(void) +{ + for (UInt32 i = 0; i < m_Recipes.size(); i++) + { + if (!m_Recipes[i]->m_RecipeName.empty()) + { + m_RecipeNameMap.emplace(m_Recipes[i]->m_RecipeName, i); + } + } +} + + + + + void cCraftingRecipes::ClearRecipes(void) { for (cRecipes::iterator itr = m_Recipes.begin(); itr != m_Recipes.end(); ++itr) @@ -384,8 +466,15 @@ void cCraftingRecipes::AddRecipeLine(int a_LineNum, const AString & a_RecipeLine std::unique_ptr Recipe = cpp14::make_unique(); + AStringVector RecipeSplit = StringSplit(Sides[0], ":"); + const auto * resultPart = &RecipeSplit[0]; + if (RecipeSplit.size() > 1) + { + resultPart = &RecipeSplit[1]; + Recipe->m_RecipeName = RecipeSplit[0]; + } // Parse the result: - AStringVector ResultSplit = StringSplit(Sides[0], ","); + AStringVector ResultSplit = StringSplit(*resultPart, ","); if (ResultSplit.empty()) { LOGWARNING("crafting.txt: line %d: Result is empty, ignoring the recipe.", a_LineNum); @@ -1059,7 +1148,3 @@ void cCraftingRecipes::HandleDyedLeather(const cItem * a_CraftingGrid, cCrafting a_Recipe->m_Result.m_ItemColor.SetColor(result_red, result_green, result_blue); } } - - - - diff --git a/src/CraftingRecipes.h b/src/CraftingRecipes.h index 9659e53fc..7e4ac86ae 100644 --- a/src/CraftingRecipes.h +++ b/src/CraftingRecipes.h @@ -10,10 +10,6 @@ #include "Item.h" - - - - // fwd: cPlayer.h class cPlayer; @@ -104,7 +100,20 @@ protected: +/** +The crafting recipes are the configurations to build a result item out of a set +of ingredient items. + +The recipes are configured in the `crafting.txt`. When populating the crafting +grid in game (inventory or crafting table), the items are compared to the +ingredients to find a matching recipe and show and craft the result. +Each recipe is defined via the result, the ingredients and the minecraft recipe +name. + +To handle the crafting recipes internally efficient the vector index of the +`cRecipes` is used as `RecipeId`. +*/ class cCraftingRecipes { public: @@ -117,7 +126,8 @@ public: /** Returns the recipe for current crafting grid. Doesn't modify the grid. Clears a_Recipe if no recipe found. */ void GetRecipe(cPlayer & a_Player, cCraftingGrid & a_CraftingGrid, cCraftingRecipe & a_Recipe); -protected: + /** Find recipes and returns the RecipeIds which contain the new item and all ingredients are in the known items */ + std::vector FindNewRecipesForItem(const cItem & a_Item, const std::set & a_KnownItems); struct cRecipeSlot { @@ -132,11 +142,21 @@ protected: { cRecipeSlots m_Ingredients; cItem m_Result; + AString m_RecipeName; // Size of the regular items in the recipe; "anywhere" items are excluded: int m_Width; int m_Height; } ; + + /** Returns the recipe by id */ + cRecipe * GetRecipeById(UInt32 a_RecipeId); + + /** Gets a map of all recipes with name and recipe id */ + const std::map & GetRecipeNameMap(); + +protected: + typedef std::vector cRecipes; cRecipes m_Recipes; @@ -170,8 +190,22 @@ protected: /** Searches for anything dye related for leather, calculates the appropriate color value, and sets the resulting value. */ void HandleDyedLeather(const cItem * a_CraftingGrid, cCraftingRecipes::cRecipe * a_Recipe, int a_GridStride, int a_GridWidth, int a_GridHeight); -} ; - - - +private: + /** Mapping the minecraft recipe names to the internal cuberite recipe Ids */ + std::map m_RecipeNameMap; + + /** + Checks if all ingredients of the a_Recipe are within the a_KnownItems list and + if the a_NewItem is part of the ingredients. + This makes sure to only find 'newly discovered' recipes. + */ + bool IsNewCraftableRecipe( + const cRecipe * a_Recipe, + const cItem & a_NewItem, + const std::set & a_KnownItems + ); + + /** Populates the RecipeNameMap */ + void PopulateRecipeNameMap(void); +} ; diff --git a/src/Entities/Player.cpp b/src/Entities/Player.cpp index d5d9f49af..df410575e 100644 --- a/src/Entities/Player.cpp +++ b/src/Entities/Player.cpp @@ -31,6 +31,8 @@ #include "../JsonUtils.h" #include "json/json.h" +#include "../CraftingRecipes.h" + // 6000 ticks or 5 minutes #define PLAYER_INVENTORY_SAVE_INTERVAL 6000 @@ -194,6 +196,18 @@ bool cPlayer::Initialize(OwnedEntity a_Self, cWorld & a_World) cPluginManager::Get()->CallHookSpawnedEntity(*GetWorld(), *this); + if (m_KnownRecipes.empty()) + { + m_ClientHandle->SendInitRecipes(0); + } + else + { + for (const auto KnownRecipe : m_KnownRecipes) + { + m_ClientHandle->SendInitRecipes(KnownRecipe); + } + } + return true; } @@ -201,6 +215,47 @@ bool cPlayer::Initialize(OwnedEntity a_Self, cWorld & a_World) +void cPlayer::AddKnownItem(const cItem & a_Item) +{ + if (a_Item.m_ItemType < 0) + { + return; + } + + auto Response = m_KnownItems.insert(a_Item.CopyOne()); + if (!Response.second) + { + // The item was already known, bail out: + return; + } + + // Process the recipes that got unlocked by this newly-known item: + auto Recipes = cRoot::Get()->GetCraftingRecipes()->FindNewRecipesForItem(a_Item, m_KnownItems); + for (const auto & RecipeId : Recipes) + { + AddKnownRecipe(RecipeId); + } +} + + + + + +void cPlayer::AddKnownRecipe(UInt32 a_RecipeId) +{ + auto Response = m_KnownRecipes.insert(a_RecipeId); + if (!Response.second) + { + // The recipe was already known, bail out: + return; + } + m_ClientHandle->SendUnlockRecipe(a_RecipeId); +} + + + + + cPlayer::~cPlayer(void) { if (!cRoot::Get()->GetPluginManager()->CallHookPlayerDestroyed(*this)) @@ -2229,6 +2284,26 @@ bool cPlayer::LoadFromFile(const AString & a_FileName, cWorldPtr & a_World) m_CurrentXp = root.get("xpCurrent", 0).asInt(); m_IsFlying = root.get("isflying", 0).asBool(); + Json::Value & JSON_KnownItems = root["knownItems"]; + for (UInt32 i = 0; i < JSON_KnownItems.size(); i++) + { + cItem Item; + Item.FromJson(JSON_KnownItems[i]); + m_KnownItems.insert(Item); + } + + const auto & RecipeNameMap = cRoot::Get()->GetCraftingRecipes()->GetRecipeNameMap(); + + Json::Value & JSON_KnownRecipes = root["knownRecipes"]; + for (UInt32 i = 0; i < JSON_KnownRecipes.size(); i++) + { + auto RecipeId = RecipeNameMap.find(JSON_KnownRecipes[i].asString()); + if (RecipeId != RecipeNameMap.end()) + { + m_KnownRecipes.insert(RecipeId->second); + } + } + m_GameMode = static_cast(root.get("gamemode", eGameMode_NotSet).asInt()); if (m_GameMode == eGameMode_Creative) @@ -2327,10 +2402,27 @@ bool cPlayer::SaveToDisk() Json::Value JSON_EnderChestInventory; cEnderChestEntity::SaveToJson(JSON_EnderChestInventory, m_EnderChestContents); + Json::Value JSON_KnownItems; + for (const auto & KnownItem : m_KnownItems) + { + Json::Value JSON_Item; + KnownItem.GetJson(JSON_Item); + JSON_KnownItems.append(JSON_Item); + } + + Json::Value JSON_KnownRecipes; + for (auto KnownRecipe : m_KnownRecipes) + { + auto Recipe = cRoot::Get()->GetCraftingRecipes()->GetRecipeById(KnownRecipe); + JSON_KnownRecipes.append(Recipe->m_RecipeName); + } + Json::Value root; root["position"] = JSON_PlayerPosition; root["rotation"] = JSON_PlayerRotation; root["inventory"] = JSON_Inventory; + root["knownItems"] = JSON_KnownItems; + root["knownRecipes"] = JSON_KnownRecipes; root["equippedItemSlot"] = m_Inventory.GetEquippedSlotNum(); root["enderchestinventory"] = JSON_EnderChestInventory; root["health"] = m_Health; @@ -3095,13 +3187,3 @@ float cPlayer::GetExplosionExposureRate(Vector3d a_ExplosionPosition, float a_Ex return Super::GetExplosionExposureRate(a_ExplosionPosition, a_ExlosionPower) / 30.0f; } - - - - - - - - - - diff --git a/src/Entities/Player.h b/src/Entities/Player.h index 11d448b11..c52d6bbdc 100644 --- a/src/Entities/Player.h +++ b/src/Entities/Player.h @@ -600,6 +600,10 @@ public: /** get player explosion exposure rate */ virtual float GetExplosionExposureRate(Vector3d a_ExplosionPosition, float a_ExlosionPower) override; + /** Adds an Item to the list of known items. + If the item is already known, does nothing. */ + void AddKnownItem(const cItem & a_Item); + protected: typedef std::vector > AStringVectorVector; @@ -753,6 +757,12 @@ protected: /** The main hand of the player */ eMainHand m_MainHand; + /** List on known recipes as Ids */ + std::set m_KnownRecipes; + + /** List of known items as Ids */ + std::set m_KnownItems; + virtual void DoMoveToWorld(const cEntity::sWorldChangeInfo & a_WorldChangeInfo) override; /** Sets the speed and sends it to the client, so that they are forced to move so. */ @@ -795,4 +805,8 @@ private: If he is not on ground it also gets divided by 5. */ float GetDigSpeed(BLOCKTYPE a_Block); + /** Add the recipe Id to the known recipes. + If the recipe is already known, does nothing. */ + void AddKnownRecipe(UInt32 RecipeId); + } ; // tolua_export diff --git a/src/Inventory.cpp b/src/Inventory.cpp index 42c243f17..6509dfe5d 100644 --- a/src/Inventory.cpp +++ b/src/Inventory.cpp @@ -105,6 +105,8 @@ int cInventory::HowManyCanFit(const cItem & a_ItemStack, int a_BeginSlotNum, int int cInventory::AddItem(const cItem & a_Item, bool a_AllowNewStacks) { + m_Owner.AddKnownItem(a_Item); + cItem ToAdd(a_Item); int res = 0; @@ -207,6 +209,26 @@ int cInventory::RemoveItem(const cItem & a_ItemStack) +cItem * cInventory::FindItem(const cItem & a_RecipeItem) +{ + cItem * Item = m_ShieldSlots.FindItem(a_RecipeItem); + if (Item != nullptr) + { + return Item; + } + Item = m_HotbarSlots.FindItem(a_RecipeItem); + if (Item != nullptr) + { + return Item; + } + + return m_InventorySlots.FindItem(a_RecipeItem); +} + + + + + bool cInventory::RemoveOneEquippedItem(void) { if (m_HotbarSlots.GetSlot(m_EquippedSlotNum).IsEmpty()) @@ -863,7 +885,3 @@ void cInventory::OnSlotChanged(cItemGrid * a_ItemGrid, int a_SlotNum) SendSlot(Base + a_SlotNum); } - - - - diff --git a/src/Inventory.h b/src/Inventory.h index 7436d7528..7221ded00 100644 --- a/src/Inventory.h +++ b/src/Inventory.h @@ -88,6 +88,9 @@ public: Returns the number of items that were removed. */ int RemoveItem(const cItem & a_ItemStack); + /** Finds an item based on ItemType and ItemDamage (<- defines the itemType, too) */ + cItem * FindItem(const cItem & a_RecipeItem); + /** Removes one item out of the currently equipped item stack, returns true if successful, false if empty-handed */ bool RemoveOneEquippedItem(void); @@ -210,7 +213,3 @@ protected: // cItemGrid::cListener override: virtual void OnSlotChanged(cItemGrid * a_ItemGrid, int a_SlotNum) override; }; // tolua_export - - - - diff --git a/src/Item.h b/src/Item.h index 175f044b5..2f7b1a238 100644 --- a/src/Item.h +++ b/src/Item.h @@ -169,6 +169,31 @@ public: AStringVector m_LoreTable; // Exported in ManualBindings.cpp + /** + Compares two items for the same type or category. Type of item is defined + via `m_ItemType` and `m_ItemDamage`. Some items (e.g. planks) have the same + `m_ItemType` and the wood kind is defined via `m_ItemDamage`. `-1` is used + as placeholder for all kinds (e.g. all kind of planks). + + Items are different when the `ItemType` is different or the `ItemDamage` + is different and unequal -1. + */ + struct sItemCompare + { + bool operator() (const cItem & a_Lhs, const cItem & a_Rhs) const + { + if (a_Lhs.m_ItemType != a_Rhs.m_ItemType) + { + return (a_Lhs.m_ItemType < a_Rhs.m_ItemType); + } + if ((a_Lhs.m_ItemDamage == -1) || (a_Rhs.m_ItemDamage == -1)) + { + return false; // -1 is a wildcard, damage of -1 alway compares equal + } + return (a_Lhs.m_ItemDamage < a_Rhs.m_ItemDamage); + } + }; + // tolua_begin int m_RepairCost; diff --git a/src/ItemGrid.cpp b/src/ItemGrid.cpp index 045f083c8..790f078fc 100644 --- a/src/ItemGrid.cpp +++ b/src/ItemGrid.cpp @@ -440,6 +440,31 @@ int cItemGrid::RemoveItem(const cItem & a_ItemStack) +cItem * cItemGrid::FindItem(const cItem & a_RecipeItem) +{ + if (!m_Slots.IsStorageAllocated()) + { + return nullptr; + } + + for (int i = 0; i < m_Slots.size(); i++) + { + // Items are equal if none is greater the other + auto compare = cItem::sItemCompare{}; + if (!compare(a_RecipeItem, m_Slots[i]) && + !compare(m_Slots[i], a_RecipeItem)) + { + return &m_Slots[i]; + } + } + + return nullptr; +} + + + + + int cItemGrid::ChangeSlotCount(int a_SlotNum, int a_AddToCount) { if (!IsValidSlotNum(a_SlotNum)) @@ -825,7 +850,3 @@ void cItemGrid::TriggerListeners(int a_SlotNum) } // for itr - m_Listeners[] m_IsInTriggerListeners = false; } - - - - diff --git a/src/ItemGrid.h b/src/ItemGrid.h index ee2dc79f3..52cfbc84a 100644 --- a/src/ItemGrid.h +++ b/src/ItemGrid.h @@ -102,6 +102,9 @@ public: Returns the number of items that were removed. */ int RemoveItem(const cItem & a_ItemStack); + /** Finds an item based on ItemType and ItemDamage (<- defines the itemType, too) */ + cItem * FindItem(const cItem & a_RecipeItem); + /** Adds (or subtracts, if a_AddToCount is negative) to the count of items in the specified slot. If the slot is empty, ignores the call. Returns the new count. @@ -198,6 +201,3 @@ protected: int AddItemToSlot(const cItem & a_ItemStack, int a_Slot, int a_Num, int a_MaxStack); } ; // tolua_end - - - diff --git a/src/Protocol/CMakeLists.txt b/src/Protocol/CMakeLists.txt index e197853cb..40eecde07 100644 --- a/src/Protocol/CMakeLists.txt +++ b/src/Protocol/CMakeLists.txt @@ -14,6 +14,7 @@ target_sources( Protocol_1_13.cpp ProtocolPalettes.cpp ProtocolRecognizer.cpp + RecipeMapper.cpp Authenticator.h ChunkDataSerializer.h @@ -29,4 +30,5 @@ target_sources( Protocol_1_13.h ProtocolPalettes.h ProtocolRecognizer.h + RecipeMapper.h ) diff --git a/src/Protocol/Packetizer.cpp b/src/Protocol/Packetizer.cpp index 6afea8a36..12bfcc0dd 100644 --- a/src/Protocol/Packetizer.cpp +++ b/src/Protocol/Packetizer.cpp @@ -121,6 +121,7 @@ AString cPacketizer::PacketTypeToStr(cProtocol::ePacketType a_PacketType) case cProtocol::pktTimeUpdate: return "pktTimeUpdate"; case cProtocol::pktTitle: return "pktTitle"; case cProtocol::pktUnloadChunk: return "pktUnloadChunk"; + case cProtocol::pktUnlockRecipe: return "pktUnlockRecipe"; case cProtocol::pktUpdateBlockEntity: return "pktUpdateBlockEntity"; case cProtocol::pktUpdateHealth: return "pktUpdateHealth"; case cProtocol::pktUpdateScore: return "pktUpdateScore"; @@ -134,7 +135,3 @@ AString cPacketizer::PacketTypeToStr(cProtocol::ePacketType a_PacketType) } return Printf("Unknown packet type: 0x%02x", a_PacketType); } - - - - diff --git a/src/Protocol/Protocol.h b/src/Protocol/Protocol.h index 12382b954..e1d901321 100644 --- a/src/Protocol/Protocol.h +++ b/src/Protocol/Protocol.h @@ -132,6 +132,7 @@ public: pktTimeUpdate, pktTitle, pktUnloadChunk, + pktUnlockRecipe, pktUpdateBlockEntity, pktUpdateHealth, pktUpdateScore, @@ -225,6 +226,8 @@ public: virtual void SendUpdateBlockEntity (cBlockEntity & a_BlockEntity) = 0; virtual void SendUpdateSign (int a_BlockX, int a_BlockY, int a_BlockZ, const AString & a_Line1, const AString & a_Line2, const AString & a_Line3, const AString & a_Line4) = 0; virtual void SendUseBed (const cEntity & a_Entity, int a_BlockX, int a_BlockY, int a_BlockZ) = 0; + virtual void SendUnlockRecipe (UInt32 a_RecipeID) = 0; + virtual void SendInitRecipes (UInt32 a_RecipeID) = 0; virtual void SendWeather (eWeather a_Weather) = 0; virtual void SendWholeInventory (const cWindow & a_Window) = 0; virtual void SendWindowClose (const cWindow & a_Window) = 0; diff --git a/src/Protocol/ProtocolRecognizer.cpp b/src/Protocol/ProtocolRecognizer.cpp index 73f8e0ff1..3f3982c90 100644 --- a/src/Protocol/ProtocolRecognizer.cpp +++ b/src/Protocol/ProtocolRecognizer.cpp @@ -51,6 +51,7 @@ AString cProtocolRecognizer::GetVersionTextFromInt(int a_ProtocolVersion) case PROTO_VERSION_1_11_1: return "1.11.1"; case PROTO_VERSION_1_12: return "1.12"; case PROTO_VERSION_1_12_1: return "1.12.1"; + case PROTO_VERSION_1_12_2: return "1.12.2"; case PROTO_VERSION_1_13: return "1.13"; } ASSERT(!"Unknown protocol version"); @@ -921,6 +922,26 @@ void cProtocolRecognizer::SendUseBed(const cEntity & a_Entity, int a_BlockX, int +void cProtocolRecognizer::SendUnlockRecipe(UInt32 a_RecipeID) +{ + ASSERT(m_Protocol != nullptr); + m_Protocol->SendUnlockRecipe(a_RecipeID); +} + + + + + +void cProtocolRecognizer::SendInitRecipes(UInt32 a_RecipeID) +{ + ASSERT(m_Protocol != nullptr); + m_Protocol->SendInitRecipes(a_RecipeID); +} + + + + + void cProtocolRecognizer::SendWeather(eWeather a_Weather) { ASSERT(m_Protocol != nullptr); diff --git a/src/Protocol/ProtocolRecognizer.h b/src/Protocol/ProtocolRecognizer.h index f82dab08a..c5d180b44 100644 --- a/src/Protocol/ProtocolRecognizer.h +++ b/src/Protocol/ProtocolRecognizer.h @@ -127,6 +127,8 @@ public: virtual void SendUpdateBlockEntity (cBlockEntity & a_BlockEntity) override; virtual void SendUpdateSign (int a_BlockX, int a_BlockY, int a_BlockZ, const AString & a_Line1, const AString & a_Line2, const AString & a_Line3, const AString & a_Line4) override; virtual void SendUseBed (const cEntity & a_Entity, int a_BlockX, int a_BlockY, int a_BlockZ) override; + virtual void SendUnlockRecipe (UInt32 a_RecipeID) override; + virtual void SendInitRecipes (UInt32 a_RecipeID) override; virtual void SendWeather (eWeather a_Weather) override; virtual void SendWholeInventory (const cWindow & a_Window) override; virtual void SendWindowClose (const cWindow & a_Window) override; diff --git a/src/Protocol/Protocol_1_12.cpp b/src/Protocol/Protocol_1_12.cpp index dba85435b..6998f73bf 100644 --- a/src/Protocol/Protocol_1_12.cpp +++ b/src/Protocol/Protocol_1_12.cpp @@ -25,6 +25,7 @@ Implements the 1.12 protocol classes: #include "../Root.h" #include "../Server.h" #include "../ClientHandle.h" +#include "../CraftingRecipes.h" #include "../Bindings/PluginManager.h" #include "../JsonUtils.h" @@ -1007,6 +1008,7 @@ UInt32 cProtocol_1_12::GetPacketID(cProtocol::ePacketType a_Packet) case pktTeleportEntity: return 0x4b; case pktTimeUpdate: return 0x46; case pktTitle: return 0x47; + case pktUnlockRecipe: return 0x30; case pktUpdateBlockEntity: return 0x09; case pktUpdateHealth: return 0x40; case pktUpdateScore: return 0x44; @@ -1019,10 +1021,27 @@ UInt32 cProtocol_1_12::GetPacketID(cProtocol::ePacketType a_Packet) +void cProtocol_1_12::HandleCraftRecipe(cByteBuffer & a_ByteBuffer) +{ + HANDLE_READ(a_ByteBuffer, ReadBEUInt8, UInt8, WindowID); + HANDLE_READ(a_ByteBuffer, ReadVarInt, UInt32, RecipeID); + HANDLE_READ(a_ByteBuffer, ReadBool, bool, MakeAll); + auto CuberiteRecipeId = cRoot::Get()->GetRecipeMapper()->GetCuberiteRecipeId(RecipeID, m_Client->GetProtocolVersion()); + if (CuberiteRecipeId.has_value()) + { + m_Client->HandleCraftRecipe(CuberiteRecipeId.value()); + } +} + + + + + void cProtocol_1_12::HandlePacketCraftingBookData(cByteBuffer & a_ByteBuffer) { + // TODO not yet used, not sure if it is needed + // https://wiki.vg/index.php?title=Protocol&oldid=14204#Crafting_Book_Data a_ByteBuffer.SkipRead(a_ByteBuffer.GetReadableSpace() - 1); - m_Client->GetPlayer()->SendMessageInfo("The green crafting book feature is not implemented yet."); } @@ -1170,6 +1189,7 @@ UInt32 cProtocol_1_12_1::GetPacketID(ePacketType a_Packet) case pktRespawn: return 0x35; case pktScoreboardObjective: return 0x42; case pktSpawnPosition: return 0x46; + case pktUnlockRecipe: return 0x31; case pktUpdateHealth: return 0x41; case pktUpdateScore: return 0x45; case pktUseBed: return 0x30; @@ -1277,7 +1297,7 @@ bool cProtocol_1_12_1::HandlePacket(cByteBuffer & a_ByteBuffer, UInt32 a_PacketT case 0x0f: HandlePacketPlayerLook(a_ByteBuffer); return true; case 0x10: HandlePacketVehicleMove(a_ByteBuffer); return true; case 0x11: HandlePacketBoatSteer(a_ByteBuffer); return true; - case 0x12: break; // Craft Recipe Request - not yet implemented + case 0x12: HandleCraftRecipe(a_ByteBuffer); return true; case 0x13: HandlePacketPlayerAbilities(a_ByteBuffer); return true; case 0x14: HandlePacketBlockDig(a_ByteBuffer); return true; case 0x15: HandlePacketEntityAction(a_ByteBuffer); return true; @@ -1397,3 +1417,55 @@ void cProtocol_1_12_2::SendKeepAlive(UInt32 a_PingID) cPacketizer Pkt(*this, pktKeepAlive); Pkt.WriteBEInt64(a_PingID); } + + + + + +void cProtocol_1_12_2::SendUnlockRecipe(UInt32 a_RecipeID) +{ + ASSERT(m_State == 3); // In game mode? + + auto ProtocolRecipeId = cRoot::Get()->GetRecipeMapper()->GetProtocolRecipeId(a_RecipeID, m_Client->GetProtocolVersion()); + if (ProtocolRecipeId.has_value()) + { + cPacketizer Pkt(*this, pktUnlockRecipe); + Pkt.WriteVarInt32(1); + Pkt.WriteBool(true); + Pkt.WriteBool(false); + Pkt.WriteVarInt32(1); + Pkt.WriteVarInt32(ProtocolRecipeId.value()); + } +} + + + + + +void cProtocol_1_12_2::SendInitRecipes(UInt32 a_RecipeID) +{ + ASSERT(m_State == 3); // In game mode? + + auto ProtocolRecipeId = cRoot::Get()->GetRecipeMapper()->GetProtocolRecipeId(a_RecipeID, m_Client->GetProtocolVersion()); + if (!ProtocolRecipeId.has_value()) + { + return; + } + + cPacketizer Pkt(*this, pktUnlockRecipe); + Pkt.WriteVarInt32(0); + Pkt.WriteBool(true); + Pkt.WriteBool(false); + if (a_RecipeID == 0) + { + Pkt.WriteVarInt32(0); + Pkt.WriteVarInt32(0); + } + else + { + Pkt.WriteVarInt32(1); + Pkt.WriteVarInt32(ProtocolRecipeId.value()); + Pkt.WriteVarInt32(1); + Pkt.WriteVarInt32(ProtocolRecipeId.value()); + } +} diff --git a/src/Protocol/Protocol_1_12.h b/src/Protocol/Protocol_1_12.h index 38c025e9e..c1b81955a 100644 --- a/src/Protocol/Protocol_1_12.h +++ b/src/Protocol/Protocol_1_12.h @@ -20,7 +20,7 @@ Declares the 1.12 protocol classes: #include "Protocol_1_11.h" - +#include "RecipeMapper.h" @@ -36,6 +36,7 @@ public: protected: virtual bool HandlePacket(cByteBuffer & a_ByteBuffer, UInt32 a_PacketType) override; virtual void HandlePacketAdvancementTab(cByteBuffer & a_ByteBuffer); + virtual void HandleCraftRecipe(cByteBuffer & a_ByteBuffer); virtual void HandlePacketCraftingBookData(cByteBuffer & a_ByteBuffer); virtual void HandlePacketStatusRequest(cByteBuffer & a_ByteBuffer) override; virtual void WriteEntityMetadata(cPacketizer & a_Pkt, const cEntity & a_Entity) override; @@ -86,8 +87,6 @@ protected: virtual void HandlePacketKeepAlive(cByteBuffer & a_ByteBuffer) override; virtual void HandlePacketStatusRequest(cByteBuffer & a_ByteBuffer) override; virtual void SendKeepAlive(UInt32 a_PingID) override; + virtual void SendUnlockRecipe(UInt32 a_RecipeID) override; + virtual void SendInitRecipes(UInt32 a_RecipeID) override; }; - - - - diff --git a/src/Protocol/Protocol_1_13.cpp b/src/Protocol/Protocol_1_13.cpp index fc048fe70..1dcecaa4b 100644 --- a/src/Protocol/Protocol_1_13.cpp +++ b/src/Protocol/Protocol_1_13.cpp @@ -140,6 +140,7 @@ UInt32 cProtocol_1_13::GetPacketID(ePacketType a_PacketType) case pktTimeUpdate: return 0x4a; case pktTitle: return 0x4b; case pktUnloadChunk: return 0x1f; + case pktUnlockRecipe: return 0x32; case pktUpdateHealth: return 0x44; case pktUpdateScore: return 0x48; case pktUpdateSign: return GetPacketID(pktUpdateBlockEntity); diff --git a/src/Protocol/Protocol_1_8.cpp b/src/Protocol/Protocol_1_8.cpp index b5d78e457..469f01c39 100644 --- a/src/Protocol/Protocol_1_8.cpp +++ b/src/Protocol/Protocol_1_8.cpp @@ -1587,6 +1587,26 @@ void cProtocol_1_8_0::SendUseBed(const cEntity & a_Entity, int a_BlockX, int a_B +void cProtocol_1_8_0::SendUnlockRecipe(UInt32 a_RecipeID) +{ + // Client doesn't support this feature + return; +} + + + + + +void cProtocol_1_8_0::SendInitRecipes(UInt32 a_RecipeID) +{ + // Client doesn't support this feature + return; +} + + + + + void cProtocol_1_8_0::SendWeather(eWeather a_Weather) { ASSERT(m_State == 3); // In game mode? diff --git a/src/Protocol/Protocol_1_8.h b/src/Protocol/Protocol_1_8.h index 42903a921..a8232104b 100644 --- a/src/Protocol/Protocol_1_8.h +++ b/src/Protocol/Protocol_1_8.h @@ -114,6 +114,8 @@ public: virtual void SendUpdateBlockEntity (cBlockEntity & a_BlockEntity) override; virtual void SendUpdateSign (int a_BlockX, int a_BlockY, int a_BlockZ, const AString & a_Line1, const AString & a_Line2, const AString & a_Line3, const AString & a_Line4) override; virtual void SendUseBed (const cEntity & a_Entity, int a_BlockX, int a_BlockY, int a_BlockZ) override; + virtual void SendUnlockRecipe (UInt32 a_RecipeID) override; + virtual void SendInitRecipes (UInt32 a_RecipeID) override; virtual void SendWeather (eWeather a_Weather) override; virtual void SendWholeInventory (const cWindow & a_Window) override; virtual void SendWindowClose (const cWindow & a_Window) override; diff --git a/src/Protocol/Protocol_1_9.cpp b/src/Protocol/Protocol_1_9.cpp index 8327eaf40..784c26f34 100644 --- a/src/Protocol/Protocol_1_9.cpp +++ b/src/Protocol/Protocol_1_9.cpp @@ -567,6 +567,12 @@ UInt32 cProtocol_1_9_0::GetPacketID(cProtocol::ePacketType a_Packet) case pktWindowItems: return 0x14; case pktWindowOpen: return 0x13; case pktWindowProperty: return 0x15; + + // Unsupported packets + case pktUnlockRecipe: + { + break; + } } UNREACHABLE("Unsupported outgoing packet type"); } diff --git a/src/Protocol/RecipeMapper.cpp b/src/Protocol/RecipeMapper.cpp new file mode 100644 index 000000000..2757fdfd6 --- /dev/null +++ b/src/Protocol/RecipeMapper.cpp @@ -0,0 +1,128 @@ +#include "Globals.h" +#include "RecipeMapper.h" +#include "../Root.h" + +cRecipeMapper::cRecipeMapper(void) +{ + AString path = "Protocol"; + auto contents = cFile::GetFolderContents(path); + for (const auto & content: contents) + { + auto fullName = path + cFile::PathSeparator() + content; + if (cFile::IsFolder(fullName)) + { + loadRecipes(content); + } + } +} + + + + + +void cRecipeMapper::loadRecipes(const AString & a_ProtocolVersion) +{ + cFile f; + if (!f.Open("Protocol/" + a_ProtocolVersion + "/base.recipes.txt", cFile::fmRead)) + { + LOGWARNING("Cannot open file \"Protocol/%s/base.recipes.txt\", no recipe book recipes will be available!", a_ProtocolVersion); + return; + } + AString Everything; + if (!f.ReadRestOfFile(Everything)) + { + LOGWARNING("Cannot read file \"Protocol/%s/base.recipes.txt\", no recipe book recipes will be available!", a_ProtocolVersion); + return; + } + f.Close(); + + // Split it into lines, then process each line as a single recipe: + AStringVector Split = StringSplit(Everything, "\n"); + m_ProtocolVersionMap[a_ProtocolVersion] = {}; + const auto & RecipeNameMap = cRoot::Get()->GetCraftingRecipes()->GetRecipeNameMap(); + + 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(a_ProtocolVersion, LineNum, Recipe, RecipeNameMap); + } + LOG("Loaded %s %zu recipe book", a_ProtocolVersion, m_ProtocolVersionMap[a_ProtocolVersion].size()); +} + + + + + +cRecipeMapper::~cRecipeMapper() +{ +} + + + + + +void cRecipeMapper::AddRecipeLine(const AString & a_ProtocolVersion, int a_LineNum, const AString & a_RecipeLine, const std::map & a_RecipeNameMap) +{ + AStringVector Sides = StringSplit(a_RecipeLine, " "); + UInt32 Id; + if (Sides.size() != 2) + { + LOGINFO("Recipe incompletely configured %s", a_RecipeLine); + return; + } + StringToInteger(Sides[0], Id); + + auto RecipeIndex = a_RecipeNameMap.find(Sides[1]); + if (RecipeIndex == a_RecipeNameMap.end()) + { + return; + } + m_ProtocolVersionMap[a_ProtocolVersion].emplace(Id, RecipeIndex->second); +} + + + + + +std::optional cRecipeMapper::GetProtocolRecipeId(UInt32 a_RecipeId, UInt32 a_ProtocolVersion) +{ + auto ProtocolMap = m_ProtocolVersionMap.find(cRoot::Get()->GetProtocolVersionTextFromInt(static_cast(a_ProtocolVersion))); + if (ProtocolMap == m_ProtocolVersionMap.end()) + { + return {}; + } + for (const auto & item: ProtocolMap->second) + { + if (item.second == a_RecipeId) + { + return item.first; + } + } + return {}; +} + + + + + +std::optional cRecipeMapper::GetCuberiteRecipeId(UInt32 a_ProtocolRecipeId, UInt32 a_ProtocolVersion) +{ + auto ProtocolMap = m_ProtocolVersionMap.find(cRoot::Get()->GetProtocolVersionTextFromInt(static_cast(a_ProtocolVersion))); + if (ProtocolMap == m_ProtocolVersionMap.end()) + { + return {}; + } + auto Element = ProtocolMap->second.find(a_ProtocolRecipeId); + if (Element != ProtocolMap->second.end()) + { + return Element->second; + } + return {}; +} diff --git a/src/Protocol/RecipeMapper.h b/src/Protocol/RecipeMapper.h new file mode 100644 index 000000000..1cac62f92 --- /dev/null +++ b/src/Protocol/RecipeMapper.h @@ -0,0 +1,35 @@ +#pragma once + +#include "../CraftingRecipes.h" +#include + +/** +The RecipeMapper handles the translation of crafting recipes into protocol +specific recipe Ids. +The crafting recipes are identified by the RecipeId. +The actual configuration is stored in the protocol specific configuration +directory, e.g. `Server/Protocol/1.12.2/base.recipes.txt` +*/ +class cRecipeMapper +{ +public: + cRecipeMapper(void); + ~cRecipeMapper(); + + /** Translates the cuberite RecipeId to the protocol specific RecipeId */ + std::optional GetProtocolRecipeId(UInt32 a_RecipeId, UInt32 a_ProtocolVersion); + + /** Translates the protocol specific RecipeId to the cuberite RecipeId */ + std::optional GetCuberiteRecipeId(UInt32 a_ProtocolRecipeId, UInt32 a_ProtocolVersion); + +private: + /** A mapping for each protocol from the protocol specific RecipeId and the cuberite RecipeId */ + std::map> m_ProtocolVersionMap; + + /** Load Recipes from the protocol specific mapping file */ + void loadRecipes(const AString & a_ProtocolVersion); + + /** Handles a single line of the protocol specific mapping file */ + void AddRecipeLine(const AString & a_ProtocolVersion, int a_LineNum, const AString & a_RecipeLine, const std::map & a_RecipeNameMap); + +}; diff --git a/src/Root.cpp b/src/Root.cpp index 0cece9e6d..b3e7f61ee 100644 --- a/src/Root.cpp +++ b/src/Root.cpp @@ -24,6 +24,7 @@ #include "BrewingRecipes.h" #include "FurnaceRecipe.h" #include "CraftingRecipes.h" +#include "Protocol/RecipeMapper.h" #include "Bindings/PluginManager.h" #include "MonsterConfig.h" #include "Entities/Player.h" @@ -213,6 +214,7 @@ void cRoot::Start(std::unique_ptr a_OverridesRepo) m_RankManager.reset(new cRankManager()); m_RankManager->Initialize(*m_MojangAPI); m_CraftingRecipes = new cCraftingRecipes(); + m_RecipeMapper.reset(new cRecipeMapper()); m_FurnaceRecipe = new cFurnaceRecipe(); m_BrewingRecipes.reset(new cBrewingRecipes()); diff --git a/src/Root.h b/src/Root.h index 6c84e6bf7..2393871da 100644 --- a/src/Root.h +++ b/src/Root.h @@ -18,6 +18,7 @@ class cItem; class cMonsterConfig; class cBrewingRecipes; class cCraftingRecipes; +class cRecipeMapper; class cFurnaceRecipe; class cWebAdmin; class cPluginManager; @@ -89,6 +90,7 @@ public: cMonsterConfig * GetMonsterConfig(void) { return m_MonsterConfig; } cCraftingRecipes * GetCraftingRecipes(void) { return m_CraftingRecipes; } // tolua_export + cRecipeMapper * GetRecipeMapper(void) { return m_RecipeMapper.get(); } cFurnaceRecipe * GetFurnaceRecipe (void) { return m_FurnaceRecipe; } // Exported in ManualBindings.cpp with quite a different signature cBrewingRecipes * GetBrewingRecipes (void) { return m_BrewingRecipes.get(); } // Exported in ManualBindings.cpp @@ -229,6 +231,7 @@ private: cMonsterConfig * m_MonsterConfig; cCraftingRecipes * m_CraftingRecipes; + std::unique_ptr m_RecipeMapper; cFurnaceRecipe * m_FurnaceRecipe; std::unique_ptr m_BrewingRecipes; cWebAdmin * m_WebAdmin; @@ -275,8 +278,3 @@ private: static void InputThread(cRoot & a_Params); }; // tolua_export - - - - - diff --git a/src/UI/CraftingWindow.cpp b/src/UI/CraftingWindow.cpp index 34599788c..d72e13729 100644 --- a/src/UI/CraftingWindow.cpp +++ b/src/UI/CraftingWindow.cpp @@ -59,3 +59,9 @@ void cCraftingWindow::DistributeStack(cItem & a_ItemStack, int a_Slot, cPlayer & + +void cCraftingWindow::LoadRecipe(cPlayer & a_Player, UInt32 a_RecipeId) +{ + auto slotAreaCrafting = static_cast(m_SlotAreas[0]); + slotAreaCrafting->LoadRecipe(a_Player, a_RecipeId); +} diff --git a/src/UI/CraftingWindow.h b/src/UI/CraftingWindow.h index 75026dc67..b0de69704 100644 --- a/src/UI/CraftingWindow.h +++ b/src/UI/CraftingWindow.h @@ -25,8 +25,7 @@ public: cCraftingWindow(); virtual void DistributeStack(cItem & a_ItemStack, int a_Slot, cPlayer & a_Player, cSlotArea * a_ClickedArea, bool a_ShouldApply) override; -}; - - - + /** Loads the given Recipe into the crafting grid */ + void LoadRecipe(cPlayer & a_Player, UInt32 a_RecipeId); +}; diff --git a/src/UI/InventoryWindow.cpp b/src/UI/InventoryWindow.cpp index 3c787ff7c..a3122d2d9 100644 --- a/src/UI/InventoryWindow.cpp +++ b/src/UI/InventoryWindow.cpp @@ -72,3 +72,9 @@ void cInventoryWindow::DistributeStack(cItem & a_ItemStack, int a_Slot, cPlayer + +void cInventoryWindow::LoadRecipe(cPlayer & a_Player, UInt32 a_RecipeId) +{ + auto slotAreaCrafting = static_cast(m_SlotAreas[0]); + slotAreaCrafting->LoadRecipe(a_Player, a_RecipeId); +} diff --git a/src/UI/InventoryWindow.h b/src/UI/InventoryWindow.h index 108f58fa0..052439f40 100644 --- a/src/UI/InventoryWindow.h +++ b/src/UI/InventoryWindow.h @@ -26,10 +26,8 @@ public: virtual void DistributeStack(cItem & a_ItemStack, int a_Slot, cPlayer & a_Player, cSlotArea * a_ClickedArea, bool a_ShouldApply) override; + /** Loads the given Recipe into the crafting grid */ + void LoadRecipe(cPlayer & a_Player, UInt32 a_RecipeId); protected: cPlayer & m_Player; }; - - - - diff --git a/src/UI/SlotArea.cpp b/src/UI/SlotArea.cpp index 0bbfb4b12..1552d4bfe 100644 --- a/src/UI/SlotArea.cpp +++ b/src/UI/SlotArea.cpp @@ -665,6 +665,7 @@ void cSlotAreaCrafting::ShiftClickedResult(cPlayer & a_Player) { return; } + a_Player.AddKnownItem(Result); cItem * PlayerSlots = GetPlayerSlots(a_Player) + 1; for (;;) { @@ -780,6 +781,70 @@ void cSlotAreaCrafting::HandleCraftItem(const cItem & a_Result, cPlayer & a_Play +void cSlotAreaCrafting::LoadRecipe(cPlayer & a_Player, UInt32 a_RecipeId) +{ + if (a_RecipeId == 0) + { + return; + } + auto Recipe = cRoot::Get()->GetCraftingRecipes()->GetRecipeById(a_RecipeId); + + int NumItems = 0; + ClearCraftingGrid(a_Player); + + for (auto itrS = Recipe->m_Ingredients.begin(); itrS != Recipe->m_Ingredients.end(); ++itrS) + { + cItem * FoundItem = a_Player.GetInventory().FindItem(itrS->m_Item); + if (FoundItem == nullptr) + { + ClearCraftingGrid(a_Player); + break; + } + cItem Item = FoundItem->CopyOne(); + ++NumItems; + int pos = 1 + itrS->x + m_GridSize * itrS->y; + // Assuming there are ether shaped or unshaped recipes, no mixed ones + if ((itrS->x == -1) && (itrS->y == -1)) + { + pos = NumItems; + } + // Handle x wildcard + else if (itrS->x == -1) + { + for (int i = 0; i < m_GridSize; i++) + { + pos = 1 + i + m_GridSize * itrS->y; + auto itemCheck = GetSlot(pos, a_Player); + if (itemCheck->IsEmpty()) + { + break; + } + } + } + SetSlot(pos, a_Player, Item); + a_Player.GetInventory().RemoveItem(Item); + } +} + + + + + +void cSlotAreaCrafting::ClearCraftingGrid(cPlayer & a_Player) +{ + for (int pos = 1; pos <= m_GridSize * m_GridSize; pos++) + { + auto Item = GetSlot(pos, a_Player); + if (Item->m_ItemCount > 0) + { + a_Player.GetInventory().AddItem(*Item); + SetSlot(pos, a_Player, cItem()); + } + } +} + + + //////////////////////////////////////////////////////////////////////////////// // cSlotAreaAnvil: @@ -2749,8 +2814,3 @@ void cSlotAreaHorse::DistributeStack(cItem & a_ItemStack, cPlayer & a_Player, bo --a_ItemStack.m_ItemCount; } } - - - - - diff --git a/src/UI/SlotArea.h b/src/UI/SlotArea.h index 86c0afd51..d363a72e6 100644 --- a/src/UI/SlotArea.h +++ b/src/UI/SlotArea.h @@ -273,6 +273,11 @@ public: // Distributing items into this area is completely disabled virtual void DistributeStack(cItem & a_ItemStack, cPlayer & a_Player, bool a_ShouldApply, bool a_KeepEmptySlots, bool a_BackFill) override; + /** Clear the crafting grid */ + void ClearCraftingGrid(cPlayer & a_Player); + + /** Loads the given Recipe into the crafting grid */ + void LoadRecipe(cPlayer & a_Player, UInt32 a_RecipeId); protected: /** Maps player's EntityID -> current recipe. @@ -555,7 +560,3 @@ public: private: cHorse & m_Horse; }; - - - - -- cgit v1.2.3