From 8947147c25e2640519897bfdc8807e97ee291d70 Mon Sep 17 00:00:00 2001 From: KingCol13 <48412633+KingCol13@users.noreply.github.com> Date: Fri, 2 Oct 2020 00:33:32 +0300 Subject: [PATCH] Enchanting table shows detail on hover. Enchanting is deterministic. (#4937) * Use lapis for enchanting, subtract correct number of levels, ClientHandle now selects from pregenerated list. Co-authored-by: Tiger Wang --- src/Bindings/ManualBindings.cpp | 30 +++++++++- src/ClientHandle.cpp | 98 +++++++++++++++------------------ src/Enchantments.cpp | 4 +- src/Enchantments.h | 7 ++- src/Entities/Player.cpp | 23 ++++++++ src/Entities/Player.h | 8 +++ src/Item.cpp | 21 ++++--- src/Item.h | 5 +- src/Items/ItemFishingRod.h | 6 +- src/UI/EnchantingWindow.cpp | 18 +++--- src/UI/EnchantingWindow.h | 23 +++++--- src/UI/SlotArea.cpp | 81 +++++++++++++++++++++------ src/UI/SlotArea.h | 10 +++- 13 files changed, 219 insertions(+), 115 deletions(-) diff --git a/src/Bindings/ManualBindings.cpp b/src/Bindings/ManualBindings.cpp index 7f7cb8ea9..e62dab289 100644 --- a/src/Bindings/ManualBindings.cpp +++ b/src/Bindings/ManualBindings.cpp @@ -2746,6 +2746,33 @@ static int tolua_get_cItem_m_LoreTable(lua_State * tolua_S) +static int tolua_cItem_EnchantByXPLevels(lua_State * tolua_S) +{ + // Check params: + cLuaState L(tolua_S); + if ( + !L.CheckParamSelf("cItem") || + !L.CheckParamNumber(2) + ) + { + return 0; + } + + // Get the params: + cItem * Self; + int NumXPLevels; + L.GetStackValue(1, Self); + L.GetStackValue(2, NumXPLevels); + + // Call: + L.Push(Self->EnchantByXPLevels(NumXPLevels, GetRandomProvider())); + return 1; +} + + + + + static int tolua_set_cItem_m_LoreTable(lua_State * tolua_S) { // Check params: @@ -4420,7 +4447,8 @@ void cManualBindings::Bind(lua_State * tolua_S) tolua_endmodule(tolua_S); tolua_beginmodule(tolua_S, "cItem"); - tolua_variable(tolua_S, "m_LoreTable", tolua_get_cItem_m_LoreTable, tolua_set_cItem_m_LoreTable); + tolua_function(tolua_S, "EnchantByXPLevels", tolua_cItem_EnchantByXPLevels); + tolua_variable(tolua_S, "m_LoreTable", tolua_get_cItem_m_LoreTable, tolua_set_cItem_m_LoreTable); tolua_endmodule(tolua_S); tolua_beginmodule(tolua_S, "cItemGrid"); diff --git a/src/ClientHandle.cpp b/src/ClientHandle.cpp index 27b34eeec..848190127 100644 --- a/src/ClientHandle.cpp +++ b/src/ClientHandle.cpp @@ -768,79 +768,67 @@ void cClientHandle::HandleEnchantItem(UInt8 a_WindowID, UInt8 a_Enchantment) if (a_Enchantment > 2) { LOGWARNING("%s attempt to crash the server with invalid enchanting selection (%u)!", GetUsername().c_str(), a_Enchantment); - Kick("Invalid enchanting!"); + Kick("Selected invalid enchantment - hacked client?"); return; } - // Bail out if something's wrong with the window + // Bail out if something's wrong with the window: if ( (m_Player->GetWindow() == nullptr) || (m_Player->GetWindow()->GetWindowID() != a_WindowID) || (m_Player->GetWindow()->GetWindowType() != cWindow::wtEnchantment) ) { + Kick("Enchantment with invalid window - hacked client?"); return; } cEnchantingWindow * Window = static_cast(m_Player->GetWindow()); - auto Item = *Window->m_SlotArea->GetSlot(0, *m_Player); // A copy of the item to be enchanted. - short BaseEnchantmentLevel = Window->GetPropertyValue(a_Enchantment); + const auto BaseEnchantmentLevel = Window->GetProperty(a_Enchantment); - if (!Item.EnchantByXPLevels(BaseEnchantmentLevel)) + // Survival players must be checked they can afford enchantment and have lapis removed: + if (!m_Player->IsGameModeCreative()) { - // Item wasn't enchantable: - return; + const auto XpRequired = m_Player->XpForLevel(BaseEnchantmentLevel); + auto LapisStack = *Window->m_SlotArea->GetSlot(1, *m_Player); // A copy of the lapis stack. + const auto LapisRequired = a_Enchantment + 1; + + // Only allow enchantment if the player has sufficient levels and lapis to enchant: + if ((m_Player->GetCurrentXp() >= XpRequired) && (LapisStack.m_ItemCount >= LapisRequired)) + { + /** We need to reduce the player's level by the number of lapis required. + However we need to keep the resulting percentage filled the same. */ + + const auto TargetLevel = m_Player->GetXpLevel() - LapisRequired; + const auto CurrentFillPercent = m_Player->GetXpPercentage(); + + // The experience to remove in order to reach the start (0% fill) of the target level. + const auto DeltaForLevel = -m_Player->GetCurrentXp() + m_Player->XpForLevel(TargetLevel); + + // The experience to add to get the same fill percent. + const auto DeltaForPercent = CurrentFillPercent * (m_Player->XpForLevel(TargetLevel + 1) - m_Player->XpForLevel(TargetLevel)); + + // Apply the experience delta: + m_Player->DeltaExperience(DeltaForLevel + DeltaForPercent); + + // Now reduce the lapis in our stack and send it back: + LapisStack.AddCount(-LapisRequired); + Window->m_SlotArea->SetSlot(1, *m_Player, LapisStack); + } + else + { + // Not creative and can't afford enchantment, so exit: + Kick("Selected unavailable enchantment - hacked client?"); + return; + } } - const auto SetEnchantAndBroadcast = [this, &Item, Window] - { - // Set the item slot to our new enchanted item: - Window->m_SlotArea->SetSlot(0, *m_Player, Item); - Window->BroadcastWholeWindow(); + // Retrieve the enchanted item corresponding to our chosen option (top, middle, bottom) + cItem EnchantedItem = Window->m_SlotArea->SelectEnchantedOption(a_Enchantment); - // Remove enchantment choices: - Window->SetProperty(0, 0, *m_Player); - Window->SetProperty(1, 0, *m_Player); - Window->SetProperty(2, 0, *m_Player); - }; - - // Creative players can always enchant: - if (m_Player->IsGameModeCreative()) - { - SetEnchantAndBroadcast(); - return; - } - - const auto XpRequired = m_Player->XpForLevel(BaseEnchantmentLevel); - auto LapisStack = *Window->m_SlotArea->GetSlot(1, *m_Player); // A copy of the lapis stack. - const auto LapisRequired = a_Enchantment + 1; - - // Only allow enchantment if the player has sufficient levels and lapis to enchant: - if ((m_Player->GetCurrentXp() >= XpRequired) && (LapisStack.m_ItemCount >= LapisRequired)) - { - /* - We need to reduce the player's level by the number of lapis required. - However we need to keep the resulting percentage filled the same. - */ - - const auto TargetLevel = m_Player->GetXpLevel() - LapisRequired; - const auto CurrentFillPercent = m_Player->GetXpPercentage(); - - // The experience to remove in order to reach the start (0% fill) of the target level. - const auto DeltaForLevel = -m_Player->GetCurrentXp() + m_Player->XpForLevel(TargetLevel); - - // The experience to add to get the same fill percent. - const auto DeltaForPercent = CurrentFillPercent * (m_Player->XpForLevel(TargetLevel + 1) - m_Player->XpForLevel(TargetLevel)); - - // Apply the experience delta: - m_Player->DeltaExperience(DeltaForLevel + DeltaForPercent); - - // Now reduce the lapis in our stack and send it back: - LapisStack.AddCount(-LapisRequired); - Window->m_SlotArea->SetSlot(1, *m_Player, LapisStack); - - SetEnchantAndBroadcast(); - } + // Set the item slot to our new enchanted item: + Window->m_SlotArea->SetSlot(0, *m_Player, EnchantedItem); + m_Player->PermuteEnchantmentSeed(); } diff --git a/src/Enchantments.cpp b/src/Enchantments.cpp index 8eda97eba..e1609bf62 100644 --- a/src/Enchantments.cpp +++ b/src/Enchantments.cpp @@ -1170,14 +1170,14 @@ void cEnchantments::CheckEnchantmentConflictsFromVector( -cEnchantments cEnchantments::GetRandomEnchantmentFromVector(const cWeightedEnchantments & a_Enchantments) +cEnchantments cEnchantments::GetRandomEnchantmentFromVector(const cWeightedEnchantments & a_Enchantments, MTRand & a_Random) { int AllWeights = 0; for (const auto & Enchantment: a_Enchantments) { AllWeights += Enchantment.m_Weight; } - int RandomNumber = GetRandomProvider().RandInt(AllWeights - 1); + int RandomNumber = a_Random.RandInt(AllWeights - 1); for (const auto & Enchantment: a_Enchantments) { RandomNumber -= Enchantment.m_Weight; diff --git a/src/Enchantments.h b/src/Enchantments.h index 40dbee038..1f4547058 100644 --- a/src/Enchantments.h +++ b/src/Enchantments.h @@ -9,11 +9,11 @@ #pragma once #include "Defines.h" +#include "FastRandom.h" #include "WorldStorage/EnchantmentSerializer.h" - // fwd: "WorldStorage/FastNBT.h" class cFastNBTWriter; class cParsedNBT; @@ -27,6 +27,7 @@ typedef std::vector cWeightedEnchantments; + /** Class that stores item enchantments or stored-enchantments The enchantments may be serialized to a stringspec and read back from such stringspec. The format for the stringspec is "id=lvl;id=lvl;id=lvl...", with an optional semicolon at the end, @@ -139,8 +140,8 @@ public: /** Check enchantment conflicts from enchantments from the vector */ static void CheckEnchantmentConflictsFromVector(cWeightedEnchantments & a_Enchantments, const cEnchantments & a_FirstEnchantment); - /** Gets random enchantment from Vector and returns it */ - static cEnchantments GetRandomEnchantmentFromVector(const cWeightedEnchantments & a_Enchantments); + /** Gets random enchantment from Vector and returns it, with randomness derived from the provided PRNG. */ + static cEnchantments GetRandomEnchantmentFromVector(const cWeightedEnchantments & a_Enchantments, MTRand & a_Random); /** Selects one enchantment from a Vector using cNoise. Mostly used for generators. Uses the enchantments' weights for the random distribution. diff --git a/src/Entities/Player.cpp b/src/Entities/Player.cpp index bbe237d39..d7455f371 100644 --- a/src/Entities/Player.cpp +++ b/src/Entities/Player.cpp @@ -151,6 +151,8 @@ cPlayer::cPlayer(const cClientHandlePtr & a_Client, const AString & a_PlayerName SetWorld(World); // Use default world + m_EnchantmentSeed = GetRandomProvider().RandInt(); // Use a random number to seed the enchantment generator + FLOGD("Player \"{0}\" is connecting for the first time, spawning at default world spawn {1:.2f}", a_PlayerName, GetPosition() ); @@ -1861,6 +1863,25 @@ void cPlayer::SetVisible(bool a_bVisible) +MTRand cPlayer::GetEnchantmentRandomProvider() +{ + return m_EnchantmentSeed; +} + + + + + +void cPlayer::PermuteEnchantmentSeed() +{ + // Get a new random integer and save that as the seed: + m_EnchantmentSeed = GetRandomProvider().RandInt(); +} + + + + + bool cPlayer::HasPermission(const AString & a_Permission) { if (a_Permission.empty()) @@ -2277,6 +2298,7 @@ bool cPlayer::LoadFromFile(const AString & a_FileName, cWorldPtr & a_World) m_LifetimeTotalXp = root.get("xpTotal", 0).asInt(); m_CurrentXp = root.get("xpCurrent", 0).asInt(); m_IsFlying = root.get("isflying", 0).asBool(); + m_EnchantmentSeed = root.get("enchantmentSeed", GetRandomProvider().RandInt()).asUInt(); Json::Value & JSON_KnownItems = root["knownItems"]; for (UInt32 i = 0; i < JSON_KnownItems.size(); i++) @@ -2439,6 +2461,7 @@ bool cPlayer::SaveToDisk() root["SpawnY"] = GetLastBedPos().y; root["SpawnZ"] = GetLastBedPos().z; root["SpawnWorld"] = m_SpawnWorld->GetName(); + root["enchantmentSeed"] = m_EnchantmentSeed; if (m_World != nullptr) { diff --git a/src/Entities/Player.h b/src/Entities/Player.h index ba219a84d..568929f44 100644 --- a/src/Entities/Player.h +++ b/src/Entities/Player.h @@ -263,6 +263,13 @@ public: // tolua_end + /** Get a copy of the PRNG for enchanting related generation, don't use this for other purposes. + The PRNG's state is initialised with an internal seed, such that until PermuteEnchantmentSeed is called, this function returns the same PRNG. */ + MTRand GetEnchantmentRandomProvider(); + + /** Permute the seed for enchanting related PRNGs, don't use this for other purposes. */ + void PermuteEnchantmentSeed(); + /** Returns the SharedPtr to client handle associated with the player. */ cClientHandlePtr GetClientHandlePtr(void) const { return m_ClientHandle; } @@ -718,6 +725,7 @@ protected: /** Player Xp level */ int m_LifetimeTotalXp; int m_CurrentXp; + unsigned int m_EnchantmentSeed; // flag saying we need to send a xp update to client bool m_bDirtyExperience; diff --git a/src/Item.cpp b/src/Item.cpp index ca4210c75..60991d37a 100644 --- a/src/Item.cpp +++ b/src/Item.cpp @@ -429,7 +429,7 @@ int cItem::GetEnchantability() -bool cItem::EnchantByXPLevels(int a_NumXPLevels) +bool cItem::EnchantByXPLevels(int a_NumXPLevels, MTRand & a_Random) { if (!cItem::IsEnchantable(m_ItemType)) { @@ -442,9 +442,8 @@ bool cItem::EnchantByXPLevels(int a_NumXPLevels) return false; } - auto & Random = GetRandomProvider(); - int ModifiedEnchantmentLevel = a_NumXPLevels + Random.RandInt(Enchantability / 4) + Random.RandInt(Enchantability / 4) + 1; - float RandomBonus = 1.0F + (Random.RandReal() + Random.RandReal() - 1.0F) * 0.15F; + int ModifiedEnchantmentLevel = a_NumXPLevels + a_Random.RandInt(Enchantability / 4) + a_Random.RandInt(Enchantability / 4) + 1; + float RandomBonus = 1.0F + (a_Random.RandReal() + a_Random.RandReal() - 1.0F) * 0.15F; int FinalEnchantmentLevel = static_cast(ModifiedEnchantmentLevel * RandomBonus + 0.5F); cWeightedEnchantments Enchantments; @@ -455,7 +454,7 @@ bool cItem::EnchantByXPLevels(int a_NumXPLevels) m_ItemType = E_ITEM_ENCHANTED_BOOK; } - cEnchantments Enchantment1 = cEnchantments::GetRandomEnchantmentFromVector(Enchantments); + cEnchantments Enchantment1 = cEnchantments::GetRandomEnchantmentFromVector(Enchantments, a_Random); m_Enchantments.AddFromString(Enchantment1.ToString()); cEnchantments::RemoveEnchantmentWeightFromVector(Enchantments, Enchantment1); @@ -465,12 +464,12 @@ bool cItem::EnchantByXPLevels(int a_NumXPLevels) // Next Enchantment (Second) float NewEnchantmentLevel = a_NumXPLevels / 2.0f; float SecondEnchantmentChance = (NewEnchantmentLevel + 1) / 50.0f; - if (Enchantments.empty() || !Random.RandBool(SecondEnchantmentChance)) + if (Enchantments.empty() || !a_Random.RandBool(SecondEnchantmentChance)) { return true; } - cEnchantments Enchantment2 = cEnchantments::GetRandomEnchantmentFromVector(Enchantments); + cEnchantments Enchantment2 = cEnchantments::GetRandomEnchantmentFromVector(Enchantments, a_Random); m_Enchantments.AddFromString(Enchantment2.ToString()); cEnchantments::RemoveEnchantmentWeightFromVector(Enchantments, Enchantment2); @@ -480,12 +479,12 @@ bool cItem::EnchantByXPLevels(int a_NumXPLevels) // Next Enchantment (Third) NewEnchantmentLevel = NewEnchantmentLevel / 2.0f; float ThirdEnchantmentChance = (NewEnchantmentLevel + 1) / 50.0f; - if (Enchantments.empty() || !Random.RandBool(ThirdEnchantmentChance)) + if (Enchantments.empty() || !a_Random.RandBool(ThirdEnchantmentChance)) { return true; } - cEnchantments Enchantment3 = cEnchantments::GetRandomEnchantmentFromVector(Enchantments); + cEnchantments Enchantment3 = cEnchantments::GetRandomEnchantmentFromVector(Enchantments, a_Random); m_Enchantments.AddFromString(Enchantment3.ToString()); cEnchantments::RemoveEnchantmentWeightFromVector(Enchantments, Enchantment3); @@ -495,11 +494,11 @@ bool cItem::EnchantByXPLevels(int a_NumXPLevels) // Next Enchantment (Fourth) NewEnchantmentLevel = NewEnchantmentLevel / 2.0f; float FourthEnchantmentChance = (NewEnchantmentLevel + 1) / 50.0f; - if (Enchantments.empty() || !Random.RandBool(FourthEnchantmentChance)) + if (Enchantments.empty() || !a_Random.RandBool(FourthEnchantmentChance)) { return true; } - cEnchantments Enchantment4 = cEnchantments::GetRandomEnchantmentFromVector(Enchantments); + cEnchantments Enchantment4 = cEnchantments::GetRandomEnchantmentFromVector(Enchantments, a_Random); m_Enchantments.AddFromString(Enchantment4.ToString()); return true; diff --git a/src/Item.h b/src/Item.h index d3f853170..600c8b2f1 100644 --- a/src/Item.h +++ b/src/Item.h @@ -141,8 +141,9 @@ public: int GetEnchantability(); // tolua_export /** Randomly enchants the item using the specified number of XP levels. - Returns true if the item was enchanted, false if not (not enchantable / too many enchantments already). */ - bool EnchantByXPLevels(int a_NumXPLevels); // tolua_export + Returns true if the item was enchanted, false if not (not enchantable / too many enchantments already). + Randomness is derived from the provided PRNG. */ + bool EnchantByXPLevels(int a_NumXPLevels, MTRand & a_Random); // Exported in ManualBindings.cpp /** Adds this specific enchantment to this item, returning the cost. FromBook specifies whether the enchantment should be treated as coming diff --git a/src/Items/ItemFishingRod.h b/src/Items/ItemFishingRod.h index 5878890ef..85688c5c1 100644 --- a/src/Items/ItemFishingRod.h +++ b/src/Items/ItemFishingRod.h @@ -175,21 +175,21 @@ public: case 0: { cItem Bow(E_ITEM_BOW, 1, Random.RandInt(50)); - Bow.EnchantByXPLevels(Random.RandInt(22, 30)); + Bow.EnchantByXPLevels(Random.RandInt(22, 30), GetRandomProvider()); Drops.Add(Bow); break; } case 1: { cItem Book(E_ITEM_BOOK); - Book.EnchantByXPLevels(30); + Book.EnchantByXPLevels(30, GetRandomProvider()); Drops.Add(Book); break; } case 2: { cItem Rod(E_ITEM_FISHING_ROD, 1, Random.RandInt(50)); - Rod.EnchantByXPLevels(Random.RandInt(22, 30)); + Rod.EnchantByXPLevels(Random.RandInt(22, 30), GetRandomProvider()); Drops.Add(Rod); break; } diff --git a/src/UI/EnchantingWindow.cpp b/src/UI/EnchantingWindow.cpp index 4ce4bebb3..15d27fc63 100644 --- a/src/UI/EnchantingWindow.cpp +++ b/src/UI/EnchantingWindow.cpp @@ -28,13 +28,12 @@ cEnchantingWindow::cEnchantingWindow(Vector3i a_BlockPos, const AString & a_Titl void cEnchantingWindow::SetProperty(short a_Property, short a_Value, cPlayer & a_Player) { - if ((a_Property < 0) || (static_cast(a_Property) >= ARRAYCOUNT(m_PropertyValue))) + ASSERT(a_Property >= 0); + if (static_cast(a_Property) < m_PropertyValue.size()) { - ASSERT(!"a_Property is invalid"); - return; + m_PropertyValue[a_Property] = a_Value; } - m_PropertyValue[a_Property] = a_Value; Super::SetProperty(a_Property, a_Value, a_Player); } @@ -44,13 +43,12 @@ void cEnchantingWindow::SetProperty(short a_Property, short a_Value, cPlayer & a void cEnchantingWindow::SetProperty(short a_Property, short a_Value) { - if ((a_Property < 0) || (static_cast(a_Property) >= ARRAYCOUNT(m_PropertyValue))) + ASSERT(a_Property >= 0); + if (static_cast(a_Property) < m_PropertyValue.size()) { - ASSERT(!"a_Property is invalid"); - return; + m_PropertyValue[a_Property] = a_Value; } - m_PropertyValue[a_Property] = a_Value; Super::SetProperty(a_Property, a_Value); } @@ -58,9 +56,9 @@ void cEnchantingWindow::SetProperty(short a_Property, short a_Value) -short cEnchantingWindow::GetPropertyValue(short a_Property) +short cEnchantingWindow::GetProperty(short a_Property) { - if ((a_Property < 0) || (static_cast(a_Property) >= ARRAYCOUNT(m_PropertyValue))) + if ((a_Property < 0) || (static_cast(a_Property) >= m_PropertyValue.size())) { ASSERT(!"a_Property is invalid"); return 0; diff --git a/src/UI/EnchantingWindow.h b/src/UI/EnchantingWindow.h index 4de5ed00c..2b32f078a 100644 --- a/src/UI/EnchantingWindow.h +++ b/src/UI/EnchantingWindow.h @@ -15,6 +15,12 @@ +class cSlotAreaEnchanting; + + + + + class cEnchantingWindow: public cWindow { @@ -24,22 +30,23 @@ public: cEnchantingWindow(Vector3i a_BlockPos, const AString & a_Title); + /** Sends enchantment properties to the client. + If the property represents a level requirement, stores it for later GetProperty retrieval. */ virtual void SetProperty(short a_Property, short a_Value, cPlayer & a_Player) override; + /** Sends enchantment properties to the client. + If the property represents a level requirement, stores it for later GetProperty retrieval. */ virtual void SetProperty(short a_Property, short a_Value) override; - /** Return the value of a property */ - short GetPropertyValue(short a_Property); + /** Return the level requirement of the given enchantment slot. */ + short GetProperty(short a_Property); virtual void DistributeStack(cItem & a_ItemStack, int a_Slot, cPlayer & a_Player, cSlotArea * a_ClickedArea, bool a_ShouldApply) override; - cSlotArea * m_SlotArea; + cSlotAreaEnchanting * m_SlotArea; protected: - short m_PropertyValue[3]; + + std::array m_PropertyValue; Vector3i m_BlockPos; }; - - - - diff --git a/src/UI/SlotArea.cpp b/src/UI/SlotArea.cpp index 1450294a4..bb597b2c9 100644 --- a/src/UI/SlotArea.cpp +++ b/src/UI/SlotArea.cpp @@ -1678,25 +1678,59 @@ void cSlotAreaEnchanting::UpdateResult(cPlayer & a_Player) { cItem Item = *GetSlot(0, a_Player); - if (cItem::IsEnchantable(Item.m_ItemType) && Item.m_Enchantments.IsEmpty()) + if (!cItem::IsEnchantable(Item.m_ItemType) || !Item.m_Enchantments.IsEmpty()) { - int Bookshelves = std::min(GetBookshelvesCount(*a_Player.GetWorld()), 15); - - auto & Random = GetRandomProvider(); - int Base = (Random.RandInt(1, 8) + (Bookshelves / 2) + Random.RandInt(0, Bookshelves)); - int TopSlot = std::max(Base / 3, 1); - int MiddleSlot = (Base * 2) / 3 + 1; - int BottomSlot = std::max(Base, Bookshelves * 2); - - m_ParentWindow.SetProperty(0, static_cast(TopSlot), a_Player); - m_ParentWindow.SetProperty(1, static_cast(MiddleSlot), a_Player); - m_ParentWindow.SetProperty(2, static_cast(BottomSlot), a_Player); + return; } - else + + // Pseudocode found at: https://minecraft.gamepedia.com/Enchanting_mechanics + const auto Bookshelves = std::min(GetBookshelvesCount(*a_Player.GetWorld()), 15U); + + // A PRNG initialised using the player's enchantment seed. + auto Random = a_Player.GetEnchantmentRandomProvider(); + + // Calculate the levels for the offered enchantment options: + const auto Base = (Random.RandInt(1U, 8U) + (Bookshelves / 2) + Random.RandInt(0U, Bookshelves)); + const std::array OptionLevels { - m_ParentWindow.SetProperty(0, 0, a_Player); - m_ParentWindow.SetProperty(1, 0, a_Player); - m_ParentWindow.SetProperty(2, 0, a_Player); + std::max(Base / 3, 1U), + (Base * 2) / 3 + 1, + std::max(Base, Bookshelves * 2) + }; + + // Properties set according to: https://wiki.vg/Protocol#Window_Property + // Fake a "seed" for the client to draw Standard Galactic Alphabet glyphs: + m_ParentWindow.SetProperty(3, Random.RandInt(), a_Player); + + // Calculate an enchanting possibility for each option (top, middle and bottom) and send details to window: + for (short i = 0; i != OptionLevels.size(); i++) + { + // A copy of the item. + cItem EnchantedItem = Item.CopyOne(); + + // Enchant based on the number of levels: + EnchantedItem.EnchantByXPLevels(OptionLevels[i], Random); + + LOGD("Generated enchanted item %d with enchantments: %s", i, EnchantedItem.m_Enchantments.ToString()); + + // Send the level requirement for the enchantment option: + m_ParentWindow.SetProperty(i, static_cast(OptionLevels[i]), a_Player); + + // Get the first enchantment ID, which must exist: + ASSERT(EnchantedItem.m_Enchantments.begin() != EnchantedItem.m_Enchantments.end()); + const short EnchantmentID = static_cast(EnchantedItem.m_Enchantments.begin()->first); + + // Send the enchantment ID of the first enchantment on our item: + m_ParentWindow.SetProperty(4 + i, EnchantmentID, a_Player); + + const short EnchantmentLevel = static_cast(EnchantedItem.m_Enchantments.GetLevel(EnchantmentID)); + ASSERT(EnchantmentLevel > 0); + + // Send the level for the first enchantment on our item: + m_ParentWindow.SetProperty(7 + i, EnchantmentLevel, a_Player); + + // Store the item we've enchanted as an option to be retrieved later: + m_EnchantedItemOptions[i] = std::move(EnchantedItem); } } @@ -1704,9 +1738,8 @@ void cSlotAreaEnchanting::UpdateResult(cPlayer & a_Player) -int cSlotAreaEnchanting::GetBookshelvesCount(cWorld & a_World) +unsigned cSlotAreaEnchanting::GetBookshelvesCount(cWorld & a_World) { - int Bookshelves = 0; cBlockArea Area; Area.Read(a_World, m_BlockPos - Vector3i(2, 0, 2), m_BlockPos + Vector3i(2, 1, 2)); @@ -1751,6 +1784,8 @@ int cSlotAreaEnchanting::GetBookshelvesCount(cWorld & a_World) { 1, 1, 0, 1, 1, 1 }, // Bookcase at {1, 1, 0}, air at {1, 1, 1} }; + unsigned Bookshelves = 0; + for (size_t i = 0; i < ARRAYCOUNT(CheckCoords); i++) { if ( @@ -1769,6 +1804,16 @@ int cSlotAreaEnchanting::GetBookshelvesCount(cWorld & a_World) +cItem cSlotAreaEnchanting::SelectEnchantedOption(size_t a_EnchantOption) +{ + ASSERT(a_EnchantOption < m_EnchantedItemOptions.size()); + return std::move(m_EnchantedItemOptions[a_EnchantOption]); +} + + + + + //////////////////////////////////////////////////////////////////////////////// // cSlotAreaEnderChest: diff --git a/src/UI/SlotArea.h b/src/UI/SlotArea.h index d363a72e6..acf8c404b 100644 --- a/src/UI/SlotArea.h +++ b/src/UI/SlotArea.h @@ -397,14 +397,20 @@ public: virtual void OnPlayerAdded (cPlayer & a_Player) override; virtual void OnPlayerRemoved(cPlayer & a_Player) override; - /* Get the count of bookshelves who stand in the near of the enchanting table */ - int GetBookshelvesCount(cWorld & a_World); + /* Get the number of bookshelves which are near the enchanting table */ + unsigned GetBookshelvesCount(cWorld & a_World); + + /* Return the enchanted item matching the chosen option (0, 1, 2) + Ownership of the cItem is transferred to the caller. */ + cItem SelectEnchantedOption(size_t a_EnchantOption); protected: + /** Handles a click in the item slot. */ void UpdateResult(cPlayer & a_Player); Vector3i m_BlockPos; + std::array m_EnchantedItemOptions; };