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 <peterbell10@live.co.uk>

* 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 <peterbell10@live.co.uk>
This commit is contained in:
Tobias Wilken
2020-07-14 17:56:42 +01:00
committed by GitHub
co-authored by peterbell10
parent 6e3f6d58a4
commit 36eab1b323
36 changed files with 1614 additions and 457 deletions
+43 -1
View File
@@ -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<cInventoryWindow *>(Window)->LoadRecipe(*m_Player, a_RecipeId);
}
else if (Window->GetWindowType() == cWindow::wtWorkbench)
{
static_cast<cCraftingWindow *>(Window)->LoadRecipe(*m_Player, a_RecipeId);
}
}
void cClientHandle::SendWeather(eWeather a_Weather)
{
m_Protocol->SendWeather(a_Weather);
+10
View File
@@ -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.
*/
+90 -5
View File
@@ -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<cItem, cItem::sItemCompare> & 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<UInt32> cCraftingRecipes::FindNewRecipesForItem(const cItem & a_Item, const std::set<cItem, cItem::sItemCompare> & a_KnownItems)
{
std::vector<UInt32> 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<AString, UInt32> & 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<cCraftingRecipes::cRecipe> Recipe = cpp14::make_unique<cCraftingRecipes::cRecipe>();
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);
}
}
+43 -9
View File
@@ -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<UInt32> FindNewRecipesForItem(const cItem & a_Item, const std::set<cItem, cItem::sItemCompare> & 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<AString, UInt32> & GetRecipeNameMap();
protected:
typedef std::vector<cRecipe *> 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<AString, UInt32> 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<cItem, cItem::sItemCompare> & a_KnownItems
);
/** Populates the RecipeNameMap */
void PopulateRecipeNameMap(void);
} ;
+92 -10
View File
@@ -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<eGameMode>(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;
}
+14
View File
@@ -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<std::vector<AString> > AStringVectorVector;
@@ -753,6 +757,12 @@ protected:
/** The main hand of the player */
eMainHand m_MainHand;
/** List on known recipes as Ids */
std::set<UInt32> m_KnownRecipes;
/** List of known items as Ids */
std::set<cItem, cItem::sItemCompare> 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
+22 -4
View File
@@ -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);
}
+3 -4
View File
@@ -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
+25
View File
@@ -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;
+25 -4
View File
@@ -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;
}
+3 -3
View File
@@ -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
+2
View File
@@ -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
)
+1 -4
View File
@@ -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);
}
+3
View File
@@ -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;
+21
View File
@@ -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);
+2
View File
@@ -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;
+74 -2
View File
@@ -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());
}
}
+4 -5
View File
@@ -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;
};
+1
View File
@@ -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);
+20
View File
@@ -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?
+2
View File
@@ -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;
+6
View File
@@ -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");
}
+128
View File
@@ -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<AString, UInt32> & a_RecipeNameMap)
{
AStringVector Sides = StringSplit(a_RecipeLine, " ");
UInt32 Id;
if (Sides.size() != 2)
{
LOGINFO("Recipe incompletely configured %s", a_RecipeLine);
return;
}
StringToInteger<UInt32>(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<UInt32> cRecipeMapper::GetProtocolRecipeId(UInt32 a_RecipeId, UInt32 a_ProtocolVersion)
{
auto ProtocolMap = m_ProtocolVersionMap.find(cRoot::Get()->GetProtocolVersionTextFromInt(static_cast<int>(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<UInt32> cRecipeMapper::GetCuberiteRecipeId(UInt32 a_ProtocolRecipeId, UInt32 a_ProtocolVersion)
{
auto ProtocolMap = m_ProtocolVersionMap.find(cRoot::Get()->GetProtocolVersionTextFromInt(static_cast<int>(a_ProtocolVersion)));
if (ProtocolMap == m_ProtocolVersionMap.end())
{
return {};
}
auto Element = ProtocolMap->second.find(a_ProtocolRecipeId);
if (Element != ProtocolMap->second.end())
{
return Element->second;
}
return {};
}
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include "../CraftingRecipes.h"
#include <optional>
/**
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<UInt32> GetProtocolRecipeId(UInt32 a_RecipeId, UInt32 a_ProtocolVersion);
/** Translates the protocol specific RecipeId to the cuberite RecipeId */
std::optional<UInt32> GetCuberiteRecipeId(UInt32 a_ProtocolRecipeId, UInt32 a_ProtocolVersion);
private:
/** A mapping for each protocol from the protocol specific RecipeId and the cuberite RecipeId */
std::map<AString, std::map<UInt32, UInt32>> 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<AString, UInt32> & a_RecipeNameMap);
};
+2
View File
@@ -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<cSettingsRepositoryInterface> 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());
+3 -5
View File
@@ -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<cRecipeMapper> m_RecipeMapper;
cFurnaceRecipe * m_FurnaceRecipe;
std::unique_ptr<cBrewingRecipes> m_BrewingRecipes;
cWebAdmin * m_WebAdmin;
@@ -275,8 +278,3 @@ private:
static void InputThread(cRoot & a_Params);
}; // tolua_export
+6
View File
@@ -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<cSlotAreaCrafting *>(m_SlotAreas[0]);
slotAreaCrafting->LoadRecipe(a_Player, a_RecipeId);
}
+3 -4
View File
@@ -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);
};
+6
View File
@@ -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<cSlotAreaCrafting *>(m_SlotAreas[0]);
slotAreaCrafting->LoadRecipe(a_Player, a_RecipeId);
}
+2 -4
View File
@@ -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;
};
+65 -5
View File
@@ -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;
}
}
+5 -4
View File
@@ -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;
};