summaryrefslogtreecommitdiffstats
path: root/src/BlockEntities
diff options
context:
space:
mode:
Diffstat (limited to 'src/BlockEntities')
-rw-r--r--src/BlockEntities/BlockEntity.cpp44
-rw-r--r--src/BlockEntities/BlockEntity.h106
-rw-r--r--src/BlockEntities/BlockEntityWithItems.h86
-rw-r--r--src/BlockEntities/ChestEntity.cpp172
-rw-r--r--src/BlockEntities/ChestEntity.h59
-rw-r--r--src/BlockEntities/DispenserEntity.cpp215
-rw-r--r--src/BlockEntities/DispenserEntity.h38
-rw-r--r--src/BlockEntities/DropSpenserEntity.cpp266
-rw-r--r--src/BlockEntities/DropSpenserEntity.h89
-rw-r--r--src/BlockEntities/DropperEntity.cpp32
-rw-r--r--src/BlockEntities/DropperEntity.h46
-rw-r--r--src/BlockEntities/FurnaceEntity.cpp479
-rw-r--r--src/BlockEntities/FurnaceEntity.h164
-rw-r--r--src/BlockEntities/HopperEntity.cpp566
-rw-r--r--src/BlockEntities/HopperEntity.h96
-rw-r--r--src/BlockEntities/JukeboxEntity.cpp125
-rw-r--r--src/BlockEntities/JukeboxEntity.h56
-rw-r--r--src/BlockEntities/NoteEntity.cpp154
-rw-r--r--src/BlockEntities/NoteEntity.h63
-rw-r--r--src/BlockEntities/SignEntity.cpp115
-rw-r--r--src/BlockEntities/SignEntity.h67
21 files changed, 3038 insertions, 0 deletions
diff --git a/src/BlockEntities/BlockEntity.cpp b/src/BlockEntities/BlockEntity.cpp
new file mode 100644
index 000000000..41a488717
--- /dev/null
+++ b/src/BlockEntities/BlockEntity.cpp
@@ -0,0 +1,44 @@
+
+// BlockEntity.cpp
+
+// Implements the cBlockEntity class that is the common ancestor for all block entities
+
+#include "Globals.h"
+#include "BlockEntity.h"
+#include "ChestEntity.h"
+#include "DispenserEntity.h"
+#include "DropperEntity.h"
+#include "FurnaceEntity.h"
+#include "HopperEntity.h"
+#include "JukeboxEntity.h"
+#include "NoteEntity.h"
+#include "SignEntity.h"
+
+
+
+
+
+cBlockEntity * cBlockEntity::CreateByBlockType(BLOCKTYPE a_BlockType, NIBBLETYPE a_BlockMeta, int a_BlockX, int a_BlockY, int a_BlockZ, cWorld * a_World)
+{
+ switch (a_BlockType)
+ {
+ case E_BLOCK_CHEST: return new cChestEntity (a_BlockX, a_BlockY, a_BlockZ, a_World);
+ case E_BLOCK_DISPENSER: return new cDispenserEntity(a_BlockX, a_BlockY, a_BlockZ, a_World);
+ case E_BLOCK_DROPPER: return new cDropperEntity (a_BlockX, a_BlockY, a_BlockZ, a_World);
+ case E_BLOCK_LIT_FURNACE: return new cFurnaceEntity (a_BlockX, a_BlockY, a_BlockZ, a_BlockType, a_BlockMeta, a_World);
+ case E_BLOCK_FURNACE: return new cFurnaceEntity (a_BlockX, a_BlockY, a_BlockZ, a_BlockType, a_BlockMeta, a_World);
+ case E_BLOCK_HOPPER: return new cHopperEntity (a_BlockX, a_BlockY, a_BlockZ, a_World);
+ case E_BLOCK_SIGN_POST: return new cSignEntity (a_BlockType, a_BlockX, a_BlockY, a_BlockZ, a_World);
+ case E_BLOCK_WALLSIGN: return new cSignEntity (a_BlockType, a_BlockX, a_BlockY, a_BlockZ, a_World);
+ case E_BLOCK_NOTE_BLOCK: return new cNoteEntity (a_BlockX, a_BlockY, a_BlockZ, a_World);
+ case E_BLOCK_JUKEBOX: return new cJukeboxEntity (a_BlockX, a_BlockY, a_BlockZ, a_World);
+ }
+ LOGD("%s: Requesting creation of an unknown block entity - block type %d (%s)",
+ __FUNCTION__, a_BlockType, ItemTypeToString(a_BlockType).c_str()
+ );
+ return NULL;
+}
+
+
+
+
diff --git a/src/BlockEntities/BlockEntity.h b/src/BlockEntities/BlockEntity.h
new file mode 100644
index 000000000..0d358b556
--- /dev/null
+++ b/src/BlockEntities/BlockEntity.h
@@ -0,0 +1,106 @@
+
+#pragma once
+
+#include "../ClientHandle.h"
+#include "../World.h"
+
+
+
+
+
+namespace Json
+{
+ class Value;
+};
+
+class cPlayer;
+class cPacket;
+
+
+
+
+
+// tolua_begin
+class cBlockEntity
+{
+protected:
+ cBlockEntity(BLOCKTYPE a_BlockType, int a_BlockX, int a_BlockY, int a_BlockZ, cWorld * a_World) :
+ m_PosX(a_BlockX),
+ m_PosY(a_BlockY),
+ m_PosZ(a_BlockZ),
+ m_RelX(a_BlockX - cChunkDef::Width * FAST_FLOOR_DIV(a_BlockX, cChunkDef::Width)),
+ m_RelZ(a_BlockZ - cChunkDef::Width * FAST_FLOOR_DIV(a_BlockZ, cChunkDef::Width)),
+ m_BlockType(a_BlockType),
+ m_World(a_World)
+ {
+ }
+
+public:
+ // tolua_end
+
+ virtual ~cBlockEntity() {}; // force a virtual destructor in all descendants
+
+ virtual void Destroy(void) {};
+
+ void SetWorld(cWorld * a_World)
+ {
+ m_World = a_World;
+ }
+
+ /// Creates a new block entity for the specified block type
+ /// If a_World is valid, then the entity is created bound to that world
+ /// Returns NULL for unknown block types
+ static cBlockEntity * CreateByBlockType(BLOCKTYPE a_BlockType, NIBBLETYPE a_BlockMeta, int a_BlockX, int a_BlockY, int a_BlockZ, cWorld * a_World = NULL);
+
+ static const char * GetClassStatic(void) // Needed for ManualBindings's ForEach templates
+ {
+ return "cBlockEntity";
+ }
+
+ // tolua_begin
+
+ // Position, in absolute block coordinates:
+ int GetPosX(void) const { return m_PosX; }
+ int GetPosY(void) const { return m_PosY; }
+ int GetPosZ(void) const { return m_PosZ; }
+
+ BLOCKTYPE GetBlockType(void) const { return m_BlockType; }
+
+ cWorld * GetWorld(void) const {return m_World; }
+
+ int GetChunkX(void) const { return FAST_FLOOR_DIV(m_PosX, cChunkDef::Width); }
+ int GetChunkZ(void) const { return FAST_FLOOR_DIV(m_PosZ, cChunkDef::Width); }
+
+ int GetRelX(void) const { return m_RelX; }
+ int GetRelZ(void) const { return m_RelZ; }
+
+ // tolua_end
+
+ virtual void SaveToJson (Json::Value & a_Value) = 0;
+
+ /// Called when a player uses this entity; should open the UI window
+ virtual void UsedBy( cPlayer * a_Player ) = 0;
+
+ /** Sends the packet defining the block entity to the client specified.
+ To send to all eligible clients, use cWorld::BroadcastBlockEntity()
+ */
+ virtual void SendTo(cClientHandle & a_Client) = 0;
+
+ /// Ticks the entity; returns true if the chunk should be marked as dirty as a result of this ticking. By default does nothing.
+ virtual bool Tick(float a_Dt, cChunk & a_Chunk) { return false; }
+
+protected:
+ /// Position in absolute block coordinates
+ int m_PosX, m_PosY, m_PosZ;
+
+ /// Position relative to the chunk, used to speed up ticking
+ int m_RelX, m_RelZ;
+
+ BLOCKTYPE m_BlockType;
+
+ cWorld * m_World;
+} ; // tolua_export
+
+
+
+
diff --git a/src/BlockEntities/BlockEntityWithItems.h b/src/BlockEntities/BlockEntityWithItems.h
new file mode 100644
index 000000000..0846ae17e
--- /dev/null
+++ b/src/BlockEntities/BlockEntityWithItems.h
@@ -0,0 +1,86 @@
+
+// BlockEntityWithItems.h
+
+// Declares the cBlockEntityWithItems class representing a common ancestor for all block entities that have an ItemGrid
+
+
+
+
+
+#pragma once
+
+#include "BlockEntity.h"
+#include "../ItemGrid.h"
+
+
+
+
+
+// tolua_begin
+class cBlockEntityWithItems :
+ public cBlockEntity
+ // tolua_end
+ // tolua doesn't seem to support multiple inheritance?
+ , public cItemGrid::cListener
+ // tolua_begin
+{
+ typedef cBlockEntity super;
+
+public:
+ // tolua_end
+
+ cBlockEntityWithItems(
+ BLOCKTYPE a_BlockType, // Type of the block that the entity represents
+ int a_BlockX, int a_BlockY, int a_BlockZ, // Position of the block entity
+ int a_ItemGridWidth, int a_ItemGridHeight, // Dimensions of the ItemGrid
+ cWorld * a_World // Optional world to assign to the entity
+ ) :
+ super(a_BlockType, a_BlockX, a_BlockY, a_BlockZ, a_World),
+ m_Contents(a_ItemGridWidth, a_ItemGridHeight)
+ {
+ m_Contents.AddListener(*this);
+ }
+
+ virtual void Destroy(void) override
+ {
+ // Drop the contents as pickups:
+ ASSERT(m_World != NULL);
+ cItems Pickups;
+ m_Contents.CopyToItems(Pickups);
+ m_Contents.Clear();
+ m_World->SpawnItemPickups(Pickups, m_PosX, m_PosY, m_PosZ);
+ }
+
+ // tolua_begin
+
+ const cItem & GetSlot(int a_SlotNum) const { return m_Contents.GetSlot(a_SlotNum); }
+ const cItem & GetSlot(int a_X, int a_Y) const { return m_Contents.GetSlot(a_X, a_Y); }
+
+ void SetSlot(int a_SlotNum, const cItem & a_Item) { m_Contents.SetSlot(a_SlotNum, a_Item); }
+ void SetSlot(int a_X, int a_Y, const cItem & a_Item) { m_Contents.SetSlot(a_X, a_Y, a_Item); }
+
+ /// Returns the ItemGrid used for storing the contents
+ cItemGrid & GetContents(void) { return m_Contents; }
+
+ // tolua_end
+
+ /// Const version of the GetContents() function for C++ type-safety
+ const cItemGrid & GetContents(void) const { return m_Contents; }
+
+protected:
+ cItemGrid m_Contents;
+
+ // cItemGrid::cListener overrides:
+ virtual void OnSlotChanged(cItemGrid * a_Grid, int a_SlotNum)
+ {
+ ASSERT(a_Grid == &m_Contents);
+ if (m_World != NULL)
+ {
+ m_World->MarkChunkDirty(GetChunkX(), GetChunkZ());
+ }
+ }
+} ; // tolua_export
+
+
+
+
diff --git a/src/BlockEntities/ChestEntity.cpp b/src/BlockEntities/ChestEntity.cpp
new file mode 100644
index 000000000..dfbe6ae87
--- /dev/null
+++ b/src/BlockEntities/ChestEntity.cpp
@@ -0,0 +1,172 @@
+
+#include "Globals.h" // NOTE: MSVC stupidness requires this to be the same across all modules
+
+#include "ChestEntity.h"
+#include "../Item.h"
+#include "../Entities/Player.h"
+#include "../UI/Window.h"
+#include "json/json.h"
+
+
+
+
+
+cChestEntity::cChestEntity(int a_BlockX, int a_BlockY, int a_BlockZ, cWorld * a_World) :
+ super(E_BLOCK_CHEST, a_BlockX, a_BlockY, a_BlockZ, ContentsWidth, ContentsHeight, a_World)
+{
+ cBlockEntityWindowOwner::SetBlockEntity(this);
+}
+
+
+
+
+
+cChestEntity::~cChestEntity()
+{
+ cWindow * Window = GetWindow();
+ if (Window != NULL)
+ {
+ Window->OwnerDestroyed();
+ }
+}
+
+
+
+
+
+bool cChestEntity::LoadFromJson(const Json::Value & a_Value)
+{
+ m_PosX = a_Value.get("x", 0).asInt();
+ m_PosY = a_Value.get("y", 0).asInt();
+ m_PosZ = a_Value.get("z", 0).asInt();
+
+ Json::Value AllSlots = a_Value.get("Slots", 0);
+ int SlotIdx = 0;
+ for (Json::Value::iterator itr = AllSlots.begin(); itr != AllSlots.end(); ++itr)
+ {
+ cItem Item;
+ Item.FromJson(*itr);
+ SetSlot(SlotIdx, Item);
+ SlotIdx++;
+ }
+ return true;
+}
+
+
+
+
+
+void cChestEntity::SaveToJson(Json::Value & a_Value)
+{
+ a_Value["x"] = m_PosX;
+ a_Value["y"] = m_PosY;
+ a_Value["z"] = m_PosZ;
+
+ Json::Value AllSlots;
+ for (int i = m_Contents.GetNumSlots() - 1; i >= 0; i--)
+ {
+ Json::Value Slot;
+ m_Contents.GetSlot(i).GetJson(Slot);
+ AllSlots.append(Slot);
+ }
+ a_Value["Slots"] = AllSlots;
+}
+
+
+
+
+
+void cChestEntity::SendTo(cClientHandle & a_Client)
+{
+ // The chest entity doesn't need anything sent to the client when it's created / gets in the viewdistance
+ // All the actual handling is in the cWindow UI code that gets called when the chest is rclked
+
+ UNUSED(a_Client);
+}
+
+
+
+
+
+void cChestEntity::UsedBy(cPlayer * a_Player)
+{
+ // If the window is not created, open it anew:
+ cWindow * Window = GetWindow();
+ if (Window == NULL)
+ {
+ OpenNewWindow();
+ Window = GetWindow();
+ }
+
+ // Open the window for the player:
+ if (Window != NULL)
+ {
+ if (a_Player->GetWindow() != Window)
+ {
+ a_Player->OpenWindow(Window);
+ }
+ }
+
+ // This is rather a hack
+ // Instead of marking the chunk as dirty upon chest contents change, we mark it dirty now
+ // We cannot properly detect contents change, but such a change doesn't happen without a player opening the chest first.
+ // The few false positives aren't much to worry about
+ int ChunkX, ChunkZ;
+ cChunkDef::BlockToChunk(m_PosX, m_PosZ, ChunkX, ChunkZ);
+ m_World->MarkChunkDirty(ChunkX, ChunkZ);
+}
+
+
+
+
+
+void cChestEntity::OpenNewWindow(void)
+{
+ // Callback for opening together with neighbor chest:
+ class cOpenDouble :
+ public cChestCallback
+ {
+ cChestEntity * m_ThisChest;
+ public:
+ cOpenDouble(cChestEntity * a_ThisChest) :
+ m_ThisChest(a_ThisChest)
+ {
+ }
+
+ virtual bool Item(cChestEntity * a_Chest) override
+ {
+ // The primary chest should eb the one with lesser X or Z coord:
+ cChestEntity * Primary = a_Chest;
+ cChestEntity * Secondary = m_ThisChest;
+ if (
+ (Primary->GetPosX() > Secondary->GetPosX()) ||
+ (Primary->GetPosZ() > Secondary->GetPosZ())
+ )
+ {
+ std::swap(Primary, Secondary);
+ }
+ m_ThisChest->OpenWindow(new cChestWindow(Primary, Secondary));
+ return false;
+ }
+ } ;
+
+ // Scan neighbors for adjacent chests:
+ cOpenDouble OpenDbl(this);
+ if (
+ m_World->DoWithChestAt(m_PosX - 1, m_PosY, m_PosZ, OpenDbl) ||
+ m_World->DoWithChestAt(m_PosX + 1, m_PosY, m_PosZ, OpenDbl) ||
+ m_World->DoWithChestAt(m_PosX , m_PosY, m_PosZ - 1, OpenDbl) ||
+ m_World->DoWithChestAt(m_PosX , m_PosY, m_PosZ + 1, OpenDbl)
+ )
+ {
+ // The double-chest window has been opened in the callback
+ return;
+ }
+
+ // There is no chest neighbor, open a single-chest window:
+ OpenWindow(new cChestWindow(this));
+}
+
+
+
+
diff --git a/src/BlockEntities/ChestEntity.h b/src/BlockEntities/ChestEntity.h
new file mode 100644
index 000000000..4f2c21e91
--- /dev/null
+++ b/src/BlockEntities/ChestEntity.h
@@ -0,0 +1,59 @@
+
+#pragma once
+
+#include "BlockEntityWithItems.h"
+#include "../UI/WindowOwner.h"
+
+
+
+
+
+namespace Json
+{
+ class Value;
+};
+
+class cClientHandle;
+class cServer;
+class cNBTData;
+
+
+
+
+
+class cChestEntity : // tolua_export
+ public cBlockEntityWindowOwner,
+ // tolua_begin
+ public cBlockEntityWithItems
+{
+ typedef cBlockEntityWithItems super;
+
+public:
+ enum {
+ ContentsHeight = 3,
+ ContentsWidth = 9,
+ } ;
+
+ // tolua_end
+
+ /// Constructor used for normal operation
+ cChestEntity(int a_BlockX, int a_BlockY, int a_BlockZ, cWorld * a_World);
+
+ virtual ~cChestEntity();
+
+ static const char * GetClassStatic(void) { return "cChestEntity"; }
+
+ bool LoadFromJson(const Json::Value & a_Value);
+
+ // cBlockEntity overrides:
+ virtual void SaveToJson(Json::Value & a_Value) override;
+ virtual void SendTo(cClientHandle & a_Client) override;
+ virtual void UsedBy(cPlayer * a_Player) override;
+
+ /// Opens a new chest window for this chest. Scans for neighbors to open a double chest window, if appropriate.
+ void OpenNewWindow(void);
+} ; // tolua_export
+
+
+
+
diff --git a/src/BlockEntities/DispenserEntity.cpp b/src/BlockEntities/DispenserEntity.cpp
new file mode 100644
index 000000000..374f3d6e3
--- /dev/null
+++ b/src/BlockEntities/DispenserEntity.cpp
@@ -0,0 +1,215 @@
+
+#include "Globals.h" // NOTE: MSVC stupidness requires this to be the same across all modules
+
+#include "DispenserEntity.h"
+#include "../Entities/Player.h"
+#include "../Simulator/FluidSimulator.h"
+#include "../Chunk.h"
+
+
+
+
+
+cDispenserEntity::cDispenserEntity(int a_BlockX, int a_BlockY, int a_BlockZ, cWorld * a_World) :
+ super(E_BLOCK_DISPENSER, a_BlockX, a_BlockY, a_BlockZ, a_World)
+{
+ SetBlockEntity(this); // cBlockEntityWindowOwner
+}
+
+
+
+
+
+void cDispenserEntity::DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum)
+{
+ int DispX = m_RelX;
+ int DispY = m_PosY;
+ int DispZ = m_RelZ;
+ NIBBLETYPE Meta = a_Chunk.GetMeta(m_RelX, m_PosY, m_RelZ);
+ AddDropSpenserDir(DispX, DispY, DispZ, Meta);
+ cChunk * DispChunk = a_Chunk.GetRelNeighborChunkAdjustCoords(DispX, DispZ);
+ if (DispChunk == NULL)
+ {
+ // Would dispense into / interact with a non-loaded chunk, ignore the tick
+ return;
+ }
+ BLOCKTYPE DispBlock = DispChunk->GetBlock(DispX, DispY, DispZ);
+
+ // Dispense the item:
+ switch (m_Contents.GetSlot(a_SlotNum).m_ItemType)
+ {
+ case E_ITEM_BUCKET:
+ {
+ LOGD("Dispensing empty bucket in slot %d; DispBlock is \"%s\" (%d).", a_SlotNum, ItemTypeToString(DispBlock).c_str(), DispBlock);
+ switch (DispBlock)
+ {
+ case E_BLOCK_STATIONARY_WATER:
+ case E_BLOCK_WATER:
+ {
+ if (ScoopUpLiquid(a_SlotNum, E_ITEM_WATER_BUCKET))
+ {
+ DispChunk->SetBlock(DispX, DispY, DispZ, E_BLOCK_AIR, 0);
+ }
+ break;
+ }
+ case E_BLOCK_STATIONARY_LAVA:
+ case E_BLOCK_LAVA:
+ {
+ if (ScoopUpLiquid(a_SlotNum, E_ITEM_LAVA_BUCKET))
+ {
+ DispChunk->SetBlock(DispX, DispY, DispZ, E_BLOCK_AIR, 0);
+ }
+ break;
+ }
+ default:
+ {
+ DropFromSlot(a_Chunk, a_SlotNum);
+ break;
+ }
+ }
+ break;
+ } // E_ITEM_BUCKET
+
+ case E_ITEM_WATER_BUCKET:
+ {
+ LOGD("Dispensing water bucket in slot %d; DispBlock is \"%s\" (%d).", a_SlotNum, ItemTypeToString(DispBlock).c_str(), DispBlock);
+ if (EmptyLiquidBucket(DispBlock, a_SlotNum))
+ {
+ DispChunk->SetBlock(DispX, DispY, DispZ, E_BLOCK_WATER, 0);
+ }
+ else
+ {
+ DropFromSlot(a_Chunk, a_SlotNum);
+ }
+ break;
+ }
+
+ case E_ITEM_LAVA_BUCKET:
+ {
+ LOGD("Dispensing lava bucket in slot %d; DispBlock is \"%s\" (%d).", a_SlotNum, ItemTypeToString(DispBlock).c_str(), DispBlock);
+ if (EmptyLiquidBucket(DispBlock, a_SlotNum))
+ {
+ DispChunk->SetBlock(DispX, DispY, DispZ, E_BLOCK_LAVA, 0);
+ }
+ else
+ {
+ DropFromSlot(a_Chunk, a_SlotNum);
+ }
+ break;
+ }
+
+ case E_ITEM_SPAWN_EGG:
+ {
+ double MobX = 0.5 + (DispX + DispChunk->GetPosX() * cChunkDef::Width);
+ double MobZ = 0.5 + (DispZ + DispChunk->GetPosZ() * cChunkDef::Width);
+ if (m_World->SpawnMob(MobX, DispY, MobZ, (cMonster::eType)m_Contents.GetSlot(a_SlotNum).m_ItemDamage) >= 0)
+ {
+ m_Contents.ChangeSlotCount(a_SlotNum, -1);
+ }
+ break;
+ }
+
+ case E_BLOCK_TNT:
+ {
+ // Spawn a primed TNT entity, if space allows:
+ if (DispChunk->GetBlock(DispX, DispY, DispZ) == E_BLOCK_AIR)
+ {
+ double TNTX = 0.5 + (DispX + DispChunk->GetPosX() * cChunkDef::Width);
+ double TNTZ = 0.5 + (DispZ + DispChunk->GetPosZ() * cChunkDef::Width);
+ m_World->SpawnPrimedTNT(TNTX, DispY + 0.5, TNTZ, 4, 0); // 4 seconds fuse, no initial velocity
+ m_Contents.ChangeSlotCount(a_SlotNum, -1);
+ }
+ break;
+ }
+
+ case E_ITEM_FLINT_AND_STEEL:
+ {
+ // Spawn fire if the block in front is air.
+ if (DispChunk->GetBlock(DispX, DispY, DispZ) == E_BLOCK_AIR)
+ {
+ DispChunk->SetBlock(DispX, DispY, DispZ, E_BLOCK_FIRE, 0);
+ m_Contents.SetSlot(a_SlotNum, m_Contents.GetSlot(a_SlotNum).m_ItemType, m_Contents.GetSlot(a_SlotNum).m_ItemCount, m_Contents.GetSlot(a_SlotNum).m_ItemDamage + 1);
+ // If the durability has run out destroy the item.
+ if (m_Contents.GetSlot(a_SlotNum).m_ItemDamage > 64)
+ {
+ m_Contents.ChangeSlotCount(a_SlotNum, -1);
+ }
+ }
+ break;
+ }
+
+ default:
+ {
+ DropFromSlot(a_Chunk, a_SlotNum);
+ break;
+ }
+ } // switch (ItemType)
+}
+
+
+
+
+
+
+bool cDispenserEntity::ScoopUpLiquid(int a_SlotNum, short a_BucketItemType)
+{
+ cItem LiquidBucket(a_BucketItemType, 1);
+ if (m_Contents.GetSlot(a_SlotNum).m_ItemCount == 1)
+ {
+ // Special case: replacing one empty bucket with one full bucket
+ m_Contents.SetSlot(a_SlotNum, LiquidBucket);
+ return true;
+ }
+
+ // There are stacked buckets at the selected slot, see if a full bucket will fit somewhere else
+ if (m_Contents.HowManyCanFit(LiquidBucket) < 1)
+ {
+ // Cannot fit into m_Contents
+ return false;
+ }
+
+ m_Contents.ChangeSlotCount(a_SlotNum, -1);
+ m_Contents.AddItem(LiquidBucket);
+ return true;
+}
+
+
+
+
+
+bool cDispenserEntity::EmptyLiquidBucket(BLOCKTYPE a_BlockInFront, int a_SlotNum)
+{
+ if (
+ (a_BlockInFront != E_BLOCK_AIR) &&
+ !IsBlockLiquid(a_BlockInFront) &&
+ !cFluidSimulator::CanWashAway(a_BlockInFront)
+ )
+ {
+ // Not a suitable block in front
+ return false;
+ }
+
+ cItem EmptyBucket(E_ITEM_BUCKET, 1);
+ if (m_Contents.GetSlot(a_SlotNum).m_ItemCount == 1)
+ {
+ // Change the single full bucket present into a single empty bucket
+ m_Contents.SetSlot(a_SlotNum, EmptyBucket);
+ return true;
+ }
+
+ // There are full buckets stacked at this slot, check if we can fit in the empty bucket
+ if (m_Contents.HowManyCanFit(EmptyBucket) < 1)
+ {
+ // The empty bucket wouldn't fit into m_Contents
+ return false;
+ }
+
+ // The empty bucket fits in, remove one full bucket and add the empty one
+ m_Contents.ChangeSlotCount(a_SlotNum, -1);
+ m_Contents.AddItem(EmptyBucket);
+ return true;
+}
+
+
+
+
diff --git a/src/BlockEntities/DispenserEntity.h b/src/BlockEntities/DispenserEntity.h
new file mode 100644
index 000000000..fdfe4e5b4
--- /dev/null
+++ b/src/BlockEntities/DispenserEntity.h
@@ -0,0 +1,38 @@
+
+#pragma once
+
+#include "DropSpenserEntity.h"
+
+
+
+
+
+// tolua_begin
+class cDispenserEntity :
+ public cDropSpenserEntity
+{
+ typedef cDropSpenserEntity super;
+
+public:
+
+ // tolua_end
+
+ /// Constructor used for normal operation
+ cDispenserEntity(int a_BlockX, int a_BlockY, int a_BlockZ, cWorld * a_World);
+
+ static const char * GetClassStatic(void) { return "cDispenserEntity"; }
+
+private:
+ // cDropSpenser overrides:
+ virtual void DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum) override;
+
+ /// If such a bucket can fit, adds it to m_Contents and returns true
+ bool ScoopUpLiquid(int a_SlotNum, short a_BucketItemType);
+
+ /// If the a_BlockInFront is liquidable and the empty bucket can fit, does the m_Contents processing and returns true
+ bool EmptyLiquidBucket(BLOCKTYPE a_BlockInFront, int a_SlotNum);
+} ; // tolua_export
+
+
+
+
diff --git a/src/BlockEntities/DropSpenserEntity.cpp b/src/BlockEntities/DropSpenserEntity.cpp
new file mode 100644
index 000000000..823ed598f
--- /dev/null
+++ b/src/BlockEntities/DropSpenserEntity.cpp
@@ -0,0 +1,266 @@
+
+// DropSpenserEntity.cpp
+
+// Declares the cDropSpenserEntity class representing a common ancestor to the cDispenserEntity and cDropperEntity
+// The dropper and dispenser only needs to override the DropSpenseFromSlot() function to provide the specific item behavior
+
+#include "Globals.h"
+#include "DropSpenserEntity.h"
+#include "../Entities/Player.h"
+#include "../Chunk.h"
+
+
+
+
+
+cDropSpenserEntity::cDropSpenserEntity(BLOCKTYPE a_BlockType, int a_BlockX, int a_BlockY, int a_BlockZ, cWorld * a_World) :
+ super(a_BlockType, a_BlockX, a_BlockY, a_BlockZ, ContentsWidth, ContentsHeight, a_World),
+ m_ShouldDropSpense(false),
+ m_IsPowered(false)
+{
+ SetBlockEntity(this); // cBlockEntityWindowOwner
+}
+
+
+
+
+
+cDropSpenserEntity::~cDropSpenserEntity()
+{
+ // Tell window its owner is destroyed
+ cWindow * Window = GetWindow();
+ if (Window != NULL)
+ {
+ Window->OwnerDestroyed();
+ }
+}
+
+
+
+
+
+void cDropSpenserEntity::AddDropSpenserDir(int & a_BlockX, int & a_BlockY, int & a_BlockZ, NIBBLETYPE a_Direction)
+{
+ switch (a_Direction)
+ {
+ case E_META_DROPSPENSER_FACING_YM: a_BlockY--; return;
+ case E_META_DROPSPENSER_FACING_YP: a_BlockY++; return;
+ case E_META_DROPSPENSER_FACING_ZM: a_BlockZ--; return;
+ case E_META_DROPSPENSER_FACING_ZP: a_BlockZ++; return;
+ case E_META_DROPSPENSER_FACING_XM: a_BlockX--; return;
+ case E_META_DROPSPENSER_FACING_XP: a_BlockX++; return;
+ }
+ LOGWARNING("%s: Unhandled direction: %d", __FUNCTION__, a_Direction);
+ return;
+}
+
+
+
+
+
+void cDropSpenserEntity::DropSpense(cChunk & a_Chunk)
+{
+ // Pick one of the occupied slots:
+ int OccupiedSlots[9];
+ int SlotsCnt = 0;
+ for (int i = m_Contents.GetNumSlots() - 1; i >= 0; i--)
+ {
+ if (!m_Contents.GetSlot(i).IsEmpty())
+ {
+ OccupiedSlots[SlotsCnt] = i;
+ SlotsCnt++;
+ }
+ } // for i - m_Contents[]
+
+ if (SlotsCnt == 0)
+ {
+ // Nothing in the dropspenser, play the click sound
+ m_World->BroadcastSoundEffect("random.click", m_PosX * 8, m_PosY * 8, m_PosZ * 8, 1.0f, 1.2f);
+ return;
+ }
+
+ int RandomSlot = m_World->GetTickRandomNumber(SlotsCnt - 1);
+
+ // DropSpense the item, using the specialized behavior in the subclasses:
+ DropSpenseFromSlot(a_Chunk, OccupiedSlots[RandomSlot]);
+
+ // Broadcast a smoke and click effects:
+ NIBBLETYPE Meta = a_Chunk.GetMeta(m_RelX, m_PosY, m_RelZ);
+ int SmokeDir = 0;
+ switch (Meta)
+ {
+ case E_META_DROPSPENSER_FACING_YP: SmokeDir = 4; break; // YP & YM don't have associated smoke dirs, just do 4 (centre of block)
+ case E_META_DROPSPENSER_FACING_YM: SmokeDir = 4; break;
+ case E_META_DROPSPENSER_FACING_XM: SmokeDir = 3; break;
+ case E_META_DROPSPENSER_FACING_XP: SmokeDir = 5; break;
+ case E_META_DROPSPENSER_FACING_ZM: SmokeDir = 1; break;
+ case E_META_DROPSPENSER_FACING_ZP: SmokeDir = 7; break;
+ }
+ m_World->BroadcastSoundParticleEffect(2000, m_PosX, m_PosY, m_PosZ, SmokeDir);
+ m_World->BroadcastSoundEffect("random.click", m_PosX * 8, m_PosY * 8, m_PosZ * 8, 1.0f, 1.0f);
+
+ // Update the UI window, if open:
+ cWindow * Window = GetWindow();
+ if (Window != NULL)
+ {
+ Window->BroadcastWholeWindow();
+ }
+}
+
+
+
+
+
+void cDropSpenserEntity::Activate(void)
+{
+ m_ShouldDropSpense = true;
+}
+
+
+
+
+
+void cDropSpenserEntity::SetRedstonePower(bool a_IsPowered)
+{
+ if (a_IsPowered && !m_IsPowered)
+ {
+ Activate();
+ }
+ m_IsPowered = a_IsPowered;
+}
+
+
+
+
+
+bool cDropSpenserEntity::Tick(float a_Dt, cChunk & a_Chunk)
+{
+ if (!m_ShouldDropSpense)
+ {
+ return false;
+ }
+
+ m_ShouldDropSpense = false;
+ DropSpense(a_Chunk);
+ return true;
+}
+
+
+
+
+
+bool cDropSpenserEntity::LoadFromJson(const Json::Value & a_Value)
+{
+ m_PosX = a_Value.get("x", 0).asInt();
+ m_PosY = a_Value.get("y", 0).asInt();
+ m_PosZ = a_Value.get("z", 0).asInt();
+
+ Json::Value AllSlots = a_Value.get("Slots", 0);
+ int SlotIdx = 0;
+ for (Json::Value::iterator itr = AllSlots.begin(); itr != AllSlots.end(); ++itr)
+ {
+ cItem Contents;
+ Contents.FromJson(*itr);
+ m_Contents.SetSlot(SlotIdx, Contents);
+ SlotIdx++;
+ if (SlotIdx >= m_Contents.GetNumSlots())
+ {
+ return true;
+ }
+ }
+
+ return true;
+}
+
+
+
+
+
+void cDropSpenserEntity::SaveToJson(Json::Value & a_Value)
+{
+ a_Value["x"] = m_PosX;
+ a_Value["y"] = m_PosY;
+ a_Value["z"] = m_PosZ;
+
+ Json::Value AllSlots;
+ int NumSlots = m_Contents.GetNumSlots();
+ for (int i = 0; i < NumSlots; i++)
+ {
+ Json::Value Slot;
+ m_Contents.GetSlot(i).GetJson(Slot);
+ AllSlots.append(Slot);
+ }
+ a_Value["Slots"] = AllSlots;
+}
+
+
+
+
+
+void cDropSpenserEntity::SendTo(cClientHandle & a_Client)
+{
+ // Nothing needs to be sent
+ UNUSED(a_Client);
+}
+
+
+
+
+
+void cDropSpenserEntity::UsedBy(cPlayer * a_Player)
+{
+ cWindow * Window = GetWindow();
+ if (Window == NULL)
+ {
+ OpenWindow(new cDropSpenserWindow(m_PosX, m_PosY, m_PosZ, this));
+ Window = GetWindow();
+ }
+
+ if (Window != NULL)
+ {
+ if (a_Player->GetWindow() != Window)
+ {
+ a_Player->OpenWindow(Window);
+ }
+ }
+}
+
+
+
+
+
+void cDropSpenserEntity::DropFromSlot(cChunk & a_Chunk, int a_SlotNum)
+{
+ int DispX = m_PosX;
+ int DispY = m_PosY;
+ int DispZ = m_PosZ;
+ NIBBLETYPE Meta = a_Chunk.GetMeta(m_RelX, m_PosY, m_RelZ);
+ AddDropSpenserDir(DispX, DispY, DispZ, Meta);
+
+ cItems Pickups;
+ Pickups.push_back(m_Contents.RemoveOneItem(a_SlotNum));
+
+ const int PickupSpeed = m_World->GetTickRandomNumber(4) + 2; // At least 2, at most 6
+ int PickupSpeedX = 0, PickupSpeedY = 0, PickupSpeedZ = 0;
+ switch (Meta)
+ {
+ case E_META_DROPSPENSER_FACING_YP: PickupSpeedY = PickupSpeed; break;
+ case E_META_DROPSPENSER_FACING_YM: PickupSpeedY = -PickupSpeed; break;
+ case E_META_DROPSPENSER_FACING_XM: PickupSpeedX = -PickupSpeed; break;
+ case E_META_DROPSPENSER_FACING_XP: PickupSpeedX = PickupSpeed; break;
+ case E_META_DROPSPENSER_FACING_ZM: PickupSpeedZ = -PickupSpeed; break;
+ case E_META_DROPSPENSER_FACING_ZP: PickupSpeedZ = PickupSpeed; break;
+ }
+
+ double MicroX, MicroY, MicroZ;
+ MicroX = DispX + 0.5;
+ MicroY = DispY + 0.4; // Slightly less than half, to accomodate actual texture hole on DropSpenser
+ MicroZ = DispZ + 0.5;
+
+
+ m_World->SpawnItemPickups(Pickups, MicroX, MicroY, MicroZ, PickupSpeedX, PickupSpeedY, PickupSpeedZ);
+}
+
+
+
+
diff --git a/src/BlockEntities/DropSpenserEntity.h b/src/BlockEntities/DropSpenserEntity.h
new file mode 100644
index 000000000..0e9039915
--- /dev/null
+++ b/src/BlockEntities/DropSpenserEntity.h
@@ -0,0 +1,89 @@
+
+// DropSpenser.h
+
+// Declares the cDropSpenser class representing a common ancestor to the cDispenserEntity and cDropperEntity
+// The dropper and dispenser only needs to override the DropSpenseFromSlot() function to provide the specific item behavior
+
+
+
+
+
+#pragma once
+
+#include "BlockEntityWithItems.h"
+#include "../UI/WindowOwner.h"
+
+
+
+
+
+namespace Json
+{
+ class Value;
+}
+
+class cClientHandle;
+class cServer;
+
+
+
+
+
+class cDropSpenserEntity : // tolua_export
+ public cBlockEntityWindowOwner,
+ // tolua_begin
+ public cBlockEntityWithItems
+{
+ typedef cBlockEntityWithItems super;
+
+public:
+ enum {
+ ContentsHeight = 3,
+ ContentsWidth = 3,
+ } ;
+
+ // tolua_end
+
+ cDropSpenserEntity(BLOCKTYPE a_BlockType, int a_BlockX, int a_BlockY, int a_BlockZ, cWorld * a_World);
+ virtual ~cDropSpenserEntity();
+
+ static const char * GetClassStatic(void) { return "cDropSpenserEntity"; }
+
+ bool LoadFromJson(const Json::Value & a_Value);
+
+ // cBlockEntity overrides:
+ virtual void SaveToJson(Json::Value & a_Value) override;
+ virtual bool Tick(float a_Dt, cChunk & a_Chunk) override;
+ virtual void SendTo(cClientHandle & a_Client) override;
+ virtual void UsedBy(cPlayer * a_Player) override;
+
+ // tolua_begin
+
+ /// Modifies the block coords to match the dropspenser direction given (where the dropspensed pickups should materialize)
+ void AddDropSpenserDir(int & a_BlockX, int & a_BlockY, int & a_BlockZ, NIBBLETYPE a_Direction);
+
+ /// Sets the dropspenser to dropspense an item in the next tick
+ void Activate(void);
+
+ /// Sets the internal redstone power flag to "on" or "off", depending on the parameter. Calls Activate() if appropriate
+ void SetRedstonePower(bool a_IsPowered);
+
+ // tolua_end
+
+protected:
+ bool m_ShouldDropSpense; ///< If true, the dropspenser will dropspense an item in the next tick
+ bool m_IsPowered; ///< Set to true when the dropspenser receives redstone power.
+
+ /// Does the actual work on dropspensing an item. Chooses the slot, calls DropSpenseFromSlot() and handles smoke / sound effects
+ void DropSpense(cChunk & a_Chunk);
+
+ /// Override this function to provide the specific behavior for item dropspensing (drop / shoot / pour / ...)
+ virtual void DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum) = 0;
+
+ /// Helper function, drops one item from the specified slot (like a dropper)
+ void DropFromSlot(cChunk & a_Chunk, int a_SlotNum);
+} ; // tolua_export
+
+
+
+
diff --git a/src/BlockEntities/DropperEntity.cpp b/src/BlockEntities/DropperEntity.cpp
new file mode 100644
index 000000000..5d4a8ad97
--- /dev/null
+++ b/src/BlockEntities/DropperEntity.cpp
@@ -0,0 +1,32 @@
+
+// DropperEntity.cpp
+
+// Implements the cRtopperEntity class representing a Dropper block entity
+
+#include "Globals.h"
+#include "DropperEntity.h"
+#include "../Entities/Player.h"
+#include "../Simulator/FluidSimulator.h"
+
+
+
+
+
+cDropperEntity::cDropperEntity(int a_BlockX, int a_BlockY, int a_BlockZ, cWorld * a_World) :
+ super(E_BLOCK_DROPPER, a_BlockX, a_BlockY, a_BlockZ, a_World)
+{
+ SetBlockEntity(this); // cBlockEntityWindowOwner
+}
+
+
+
+
+
+void cDropperEntity::DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum)
+{
+ DropFromSlot(a_Chunk, a_SlotNum);
+}
+
+
+
+
diff --git a/src/BlockEntities/DropperEntity.h b/src/BlockEntities/DropperEntity.h
new file mode 100644
index 000000000..8e07bc6f8
--- /dev/null
+++ b/src/BlockEntities/DropperEntity.h
@@ -0,0 +1,46 @@
+
+// DropperEntity.h
+
+// Declares the cDropperEntity class representing a dropper block entity
+
+
+
+
+
+#pragma once
+
+#include "DropSpenserEntity.h"
+
+
+
+
+
+// tolua_begin
+class cDropperEntity :
+ public cDropSpenserEntity
+{
+ typedef cDropSpenserEntity super;
+
+public:
+
+ // tolua_end
+
+ /// Constructor used for normal operation
+ cDropperEntity(int a_BlockX, int a_BlockY, int a_BlockZ, cWorld * a_World);
+
+ static const char * GetClassStatic(void) { return "cDropperEntity"; }
+
+protected:
+ // cDropSpenserEntity overrides:
+ virtual void DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum) override;
+
+ /** Takes an item from slot a_SlotNum and puts it into the container in front of the dropper.
+ Called when there's a container directly in front of the dropper,
+ so the dropper should store items there, rather than dropping.
+ */
+ void PutIntoContainer(cChunk & a_Chunk, int a_SlotNum, BLOCKTYPE a_ContainerBlock, int a_ContainerX, int a_ContainerY, int a_ContainerZ);
+} ; // tolua_export
+
+
+
+
diff --git a/src/BlockEntities/FurnaceEntity.cpp b/src/BlockEntities/FurnaceEntity.cpp
new file mode 100644
index 000000000..b1409f5cc
--- /dev/null
+++ b/src/BlockEntities/FurnaceEntity.cpp
@@ -0,0 +1,479 @@
+
+#include "Globals.h" // NOTE: MSVC stupidness requires this to be the same across all modules
+
+#include "FurnaceEntity.h"
+#include "../UI/Window.h"
+#include "../Entities/Player.h"
+#include "../Root.h"
+#include "../Chunk.h"
+#include "json/json.h"
+
+
+
+
+
+
+enum
+{
+ PROGRESSBAR_SMELTING = 0,
+ PROGRESSBAR_FUEL = 1,
+} ;
+
+
+
+
+
+cFurnaceEntity::cFurnaceEntity(int a_BlockX, int a_BlockY, int a_BlockZ, BLOCKTYPE a_BlockType, NIBBLETYPE a_BlockMeta, cWorld * a_World) :
+ super(E_BLOCK_FURNACE, a_BlockX, a_BlockY, a_BlockZ, ContentsWidth, ContentsHeight, a_World),
+ m_BlockType(a_BlockType),
+ m_BlockMeta(a_BlockMeta),
+ m_CurrentRecipe(NULL),
+ m_IsCooking((a_World->GetBlock(a_BlockX, a_BlockY, a_BlockZ) == E_BLOCK_LIT_FURNACE)),
+ m_NeedCookTime(0),
+ m_TimeCooked(0),
+ m_FuelBurnTime(0),
+ m_TimeBurned(0),
+ m_LastProgressFuel(0),
+ m_LastProgressCook(0)
+{
+ cBlockEntityWindowOwner::SetBlockEntity(this);
+ m_Contents.AddListener(*this);
+}
+
+
+
+
+
+cFurnaceEntity::~cFurnaceEntity()
+{
+ // Tell window its owner is destroyed
+ cWindow * Window = GetWindow();
+ if (Window != NULL)
+ {
+ Window->OwnerDestroyed();
+ }
+}
+
+
+
+
+
+void cFurnaceEntity::UsedBy(cPlayer * a_Player)
+{
+ if (GetWindow() == NULL)
+ {
+ OpenWindow(new cFurnaceWindow(m_PosX, m_PosY, m_PosZ, this));
+ }
+ cWindow * Window = GetWindow();
+ if (Window != NULL)
+ {
+ if (a_Player->GetWindow() != Window)
+ {
+ a_Player->OpenWindow(Window);
+ BroadcastProgress(PROGRESSBAR_FUEL, m_LastProgressFuel);
+ BroadcastProgress(PROGRESSBAR_SMELTING, m_LastProgressCook);
+ }
+ }
+}
+
+
+
+
+
+/// Restarts cooking. Used after the furnace is loaded from storage to set up the internal variables so that cooking continues, if it was active. Returns true if cooking.
+bool cFurnaceEntity::ContinueCooking(void)
+{
+ UpdateInput();
+ UpdateFuel();
+ return m_IsCooking;
+}
+
+
+
+
+
+bool cFurnaceEntity::Tick(float a_Dt, cChunk & a_Chunk)
+{
+ if (m_FuelBurnTime <= 0)
+ {
+ // No fuel is burning, reset progressbars and bail out
+ if ((m_LastProgressCook > 0) || (m_LastProgressFuel > 0))
+ {
+ UpdateProgressBars();
+ }
+ return false;
+ }
+
+ if (m_IsCooking)
+ {
+ m_TimeCooked++;
+ if (m_TimeCooked >= m_NeedCookTime)
+ {
+ // Finished smelting one item
+ FinishOne(a_Chunk);
+ }
+ }
+
+ m_TimeBurned++;
+ if (m_TimeBurned >= m_FuelBurnTime)
+ {
+ // The current fuel has been exhausted, use another one, if possible
+ BurnNewFuel();
+ }
+
+ UpdateProgressBars();
+
+ return true;
+}
+
+
+
+
+
+bool cFurnaceEntity::LoadFromJson(const Json::Value & a_Value)
+{
+ m_PosX = a_Value.get("x", 0).asInt();
+ m_PosY = a_Value.get("y", 0).asInt();
+ m_PosZ = a_Value.get("z", 0).asInt();
+
+ Json::Value AllSlots = a_Value.get("Slots", 0);
+ int SlotIdx = 0;
+ for (Json::Value::iterator itr = AllSlots.begin(); itr != AllSlots.end(); ++itr)
+ {
+ cItem Item;
+ Item.FromJson(*itr);
+ SetSlot(SlotIdx, Item);
+ SlotIdx++;
+ }
+
+ m_NeedCookTime = (int)(a_Value.get("CookTime", 0).asDouble() / 50);
+ m_TimeCooked = (int)(a_Value.get("TimeCooked", 0).asDouble() / 50);
+ m_FuelBurnTime = (int)(a_Value.get("BurnTime", 0).asDouble() / 50);
+ m_TimeBurned = (int)(a_Value.get("TimeBurned", 0).asDouble() / 50);
+
+ return true;
+}
+
+
+
+
+
+void cFurnaceEntity::SaveToJson( Json::Value& a_Value )
+{
+ a_Value["x"] = m_PosX;
+ a_Value["y"] = m_PosY;
+ a_Value["z"] = m_PosZ;
+
+ Json::Value AllSlots;
+ int NumSlots = m_Contents.GetNumSlots();
+ for (int i = 0; i < NumSlots; i++)
+ {
+ Json::Value Slot;
+ m_Contents.GetSlot(i).GetJson(Slot);
+ AllSlots.append(Slot);
+ }
+ a_Value["Slots"] = AllSlots;
+
+ a_Value["CookTime"] = m_NeedCookTime * 50;
+ a_Value["TimeCooked"] = m_TimeCooked * 50;
+ a_Value["BurnTime"] = m_FuelBurnTime * 50;
+ a_Value["TimeBurned"] = m_TimeBurned * 50;
+}
+
+
+
+
+
+void cFurnaceEntity::SendTo(cClientHandle & a_Client)
+{
+ // Nothing needs to be sent
+ UNUSED(a_Client);
+}
+
+
+
+
+
+void cFurnaceEntity::BroadcastProgress(int a_ProgressbarID, short a_Value)
+{
+ cWindow * Window = GetWindow();
+ if (Window != NULL)
+ {
+ Window->BroadcastProgress(a_ProgressbarID, a_Value);
+ }
+}
+
+
+
+
+
+/// One item finished cooking
+void cFurnaceEntity::FinishOne(cChunk & a_Chunk)
+{
+ m_TimeCooked = 0;
+
+ if (m_Contents.GetSlot(fsOutput).IsEmpty())
+ {
+ m_Contents.SetSlot(fsOutput, *m_CurrentRecipe->Out);
+ }
+ else
+ {
+ m_Contents.ChangeSlotCount(fsOutput, m_CurrentRecipe->Out->m_ItemCount);
+ }
+ m_Contents.ChangeSlotCount(fsInput, -m_CurrentRecipe->In->m_ItemCount);
+
+ UpdateIsCooking();
+}
+
+
+
+
+
+void cFurnaceEntity::BurnNewFuel(void)
+{
+ cFurnaceRecipe * FR = cRoot::Get()->GetFurnaceRecipe();
+ int NewTime = FR->GetBurnTime(m_Contents.GetSlot(fsFuel));
+ if (NewTime == 0)
+ {
+ // The item in the fuel slot is not suitable
+ m_FuelBurnTime = 0;
+ m_TimeBurned = 0;
+ SetIsCooking(false);
+ return;
+ }
+
+ // Is the input and output ready for cooking?
+ if (!CanCookInputToOutput())
+ {
+ return;
+ }
+
+ // Burn one new fuel:
+ m_FuelBurnTime = NewTime;
+ m_TimeBurned = 0;
+ SetIsCooking(true);
+ if (m_Contents.GetSlot(fsFuel).m_ItemType == E_ITEM_LAVA_BUCKET)
+ {
+ m_Contents.SetSlot(fsFuel, cItem(E_ITEM_BUCKET));
+ }
+ else
+ {
+ m_Contents.ChangeSlotCount(fsFuel, -1);
+ }
+}
+
+
+
+
+
+void cFurnaceEntity::OnSlotChanged(cItemGrid * a_ItemGrid, int a_SlotNum)
+{
+ super::OnSlotChanged(a_ItemGrid, a_SlotNum);
+
+ if (m_World == NULL)
+ {
+ // The furnace isn't initialized yet, do no processing
+ return;
+ }
+
+ ASSERT(a_ItemGrid == &m_Contents);
+ switch (a_SlotNum)
+ {
+ case fsInput:
+ {
+ UpdateInput();
+ break;
+ }
+
+ case fsFuel:
+ {
+ UpdateFuel();
+ break;
+ }
+
+ case fsOutput:
+ {
+ UpdateOutput();
+ break;
+ }
+ }
+}
+
+
+
+
+
+
+/// Updates the current recipe, based on the current input
+void cFurnaceEntity::UpdateInput(void)
+{
+ if (!m_Contents.GetSlot(fsInput).IsStackableWith(m_LastInput))
+ {
+ // The input is different from what we had before, reset the cooking time
+ m_TimeCooked = 0;
+ }
+ m_LastInput = m_Contents.GetSlot(fsInput);
+
+ cFurnaceRecipe * FR = cRoot::Get()->GetFurnaceRecipe();
+ m_CurrentRecipe = FR->GetRecipeFrom(m_Contents.GetSlot(fsInput));
+ if (!CanCookInputToOutput())
+ {
+ // This input cannot be cooked
+ m_NeedCookTime = 0;
+ SetIsCooking(false);
+ }
+ else
+ {
+ m_NeedCookTime = m_CurrentRecipe->CookTime;
+ SetIsCooking(true);
+
+ // Start burning new fuel if there's no flame now:
+ if (GetFuelBurnTimeLeft() <= 0)
+ {
+ BurnNewFuel();
+ }
+ }
+}
+
+
+
+
+
+/// Called when the fuel slot changes or when the fuel is spent, burns another piece of fuel if appropriate
+void cFurnaceEntity::UpdateFuel(void)
+{
+ if (m_FuelBurnTime > m_TimeBurned)
+ {
+ // The current fuel is still burning, don't modify anything:
+ return;
+ }
+
+ // The current fuel is spent, try to burn some more:
+ BurnNewFuel();
+}
+
+
+
+
+
+/// Called when the output slot changes; starts burning if space became available
+void cFurnaceEntity::UpdateOutput(void)
+{
+ if (!CanCookInputToOutput())
+ {
+ // Cannot cook anymore:
+ m_TimeCooked = 0;
+ m_NeedCookTime = 0;
+ SetIsCooking(false);
+ return;
+ }
+
+ // No need to burn new fuel, the Tick() function will take care of that
+
+ // Can cook, start cooking if not already underway:
+ m_NeedCookTime = m_CurrentRecipe->CookTime;
+ SetIsCooking(m_FuelBurnTime > 0);
+}
+
+
+
+
+
+/// Updates the m_IsCooking, based on the input slot, output slot and m_FuelBurnTime / m_TimeBurned
+void cFurnaceEntity::UpdateIsCooking(void)
+{
+ if (
+ !CanCookInputToOutput() || // Cannot cook this
+ (m_FuelBurnTime <= 0) || // No fuel
+ (m_TimeBurned >= m_FuelBurnTime) // Fuel burnt out
+ )
+ {
+ // Reset everything
+ SetIsCooking(false);
+ m_TimeCooked = 0;
+ m_NeedCookTime = 0;
+ return;
+ }
+
+ SetIsCooking(true);
+}
+
+
+
+
+
+/// Returns true if the input can be cooked into output and the item counts allow for another cooking operation
+bool cFurnaceEntity::CanCookInputToOutput(void) const
+{
+ if (m_CurrentRecipe == NULL)
+ {
+ // This input cannot be cooked
+ return false;
+ }
+
+ if (m_Contents.GetSlot(fsOutput).IsEmpty())
+ {
+ // The output is empty, can cook
+ return true;
+ }
+
+ if (!m_Contents.GetSlot(fsOutput).IsStackableWith(*m_CurrentRecipe->Out))
+ {
+ // The output slot is blocked with something that cannot be stacked with the recipe's output
+ return false;
+ }
+
+ if (m_Contents.GetSlot(fsOutput).IsFullStack())
+ {
+ // Cannot add any more items to the output slot
+ return false;
+ }
+
+ return true;
+}
+
+
+
+
+
+/// Broadcasts progressbar updates, if needed
+void cFurnaceEntity::UpdateProgressBars(void)
+{
+ // In order to preserve bandwidth, an update is sent only every 10th tick
+ // That's why the comparisons use the division by eight
+
+ int CurFuel = (m_FuelBurnTime > 0) ? (200 - 200 * m_TimeBurned / m_FuelBurnTime) : 0;
+ if ((CurFuel / 8) != (m_LastProgressFuel / 8))
+ {
+ BroadcastProgress(PROGRESSBAR_FUEL, CurFuel);
+ m_LastProgressFuel = CurFuel;
+ }
+
+ int CurCook = (m_NeedCookTime > 0) ? (200 * m_TimeCooked / m_NeedCookTime) : 0;
+ if ((CurCook / 8) != (m_LastProgressCook / 8))
+ {
+ BroadcastProgress(PROGRESSBAR_SMELTING, CurCook);
+ m_LastProgressCook = CurCook;
+ }
+}
+
+
+
+
+
+void cFurnaceEntity::SetIsCooking(bool a_IsCooking)
+{
+ if (a_IsCooking == m_IsCooking)
+ {
+ return;
+ }
+
+ m_IsCooking = a_IsCooking;
+
+ // Light or extinguish the furnace:
+ m_World->FastSetBlock(m_PosX, m_PosY, m_PosZ, m_IsCooking ? E_BLOCK_LIT_FURNACE : E_BLOCK_FURNACE, m_BlockMeta);
+}
+
+
+
+
diff --git a/src/BlockEntities/FurnaceEntity.h b/src/BlockEntities/FurnaceEntity.h
new file mode 100644
index 000000000..9464fd175
--- /dev/null
+++ b/src/BlockEntities/FurnaceEntity.h
@@ -0,0 +1,164 @@
+
+#pragma once
+
+#include "BlockEntityWithItems.h"
+#include "../UI/WindowOwner.h"
+#include "../FurnaceRecipe.h"
+
+
+
+
+
+namespace Json
+{
+ class Value;
+}
+
+class cClientHandle;
+class cServer;
+
+
+
+
+
+class cFurnaceEntity : // tolua_export
+ public cBlockEntityWindowOwner,
+ // tolua_begin
+ public cBlockEntityWithItems
+{
+ typedef cBlockEntityWithItems super;
+
+public:
+ enum
+ {
+ fsInput = 0, // Input slot number
+ fsFuel = 1, // Fuel slot number
+ fsOutput = 2, // Output slot number
+
+ ContentsWidth = 3,
+ ContentsHeight = 1,
+ };
+
+ // tolua_end
+
+ /// Constructor used for normal operation
+ cFurnaceEntity(int a_BlockX, int a_BlockY, int a_BlockZ, BLOCKTYPE a_BlockType, NIBBLETYPE a_BlockMeta, cWorld * a_World);
+
+ virtual ~cFurnaceEntity();
+
+ static const char * GetClassStatic() { return "cFurnaceEntity"; }
+
+ bool LoadFromJson(const Json::Value & a_Value);
+
+ // cBlockEntity overrides:
+ virtual void SaveToJson(Json::Value & a_Value) override;
+ virtual void SendTo(cClientHandle & a_Client) override;
+ virtual bool Tick(float a_Dt, cChunk & a_Chunk) override;
+ virtual void UsedBy(cPlayer * a_Player) override;
+
+ /// Restarts cooking. Used after the furnace is loaded from storage to set up the internal variables so that cooking continues, if it was active. Returns true if cooking.
+ bool ContinueCooking(void);
+
+ void ResetCookTimer();
+
+ // tolua_begin
+
+ /// Returns the item in the input slot
+ const cItem & GetInputSlot(void) const { return GetSlot(fsInput); }
+
+ /// Returns the item in the fuel slot
+ const cItem & GetFuelSlot(void) const { return GetSlot(fsFuel); }
+
+ /// Returns the item in the output slot
+ const cItem & GetOutputSlot(void) const { return GetSlot(fsOutput); }
+
+ /// Sets the item in the input slot
+ void SetInputSlot(const cItem & a_Item) { SetSlot(fsInput, a_Item); }
+
+ /// Sets the item in the fuel slot
+ void SetFuelSlot(const cItem & a_Item) { SetSlot(fsFuel, a_Item); }
+
+ /// Sets the item in the output slot
+ void SetOutputSlot(const cItem & a_Item) { SetSlot(fsOutput, a_Item); }
+
+ /// Returns the time that the current item has been cooking, in ticks
+ int GetTimeCooked(void) const {return m_TimeCooked; }
+
+ /// Returns the time until the current item finishes cooking, in ticks
+ int GetCookTimeLeft(void) const { return m_NeedCookTime - m_TimeCooked; }
+
+ /// Returns the time until the current fuel is depleted, in ticks
+ int GetFuelBurnTimeLeft(void) const {return m_FuelBurnTime - m_TimeBurned; }
+
+ /// Returns true if there's time left before the current fuel is depleted
+ bool HasFuelTimeLeft(void) const { return (GetFuelBurnTimeLeft() > 0); }
+
+ // tolua_end
+
+ void SetBurnTimes(int a_FuelBurnTime, int a_TimeBurned) {m_FuelBurnTime = a_FuelBurnTime; m_TimeBurned = 0; }
+ void SetCookTimes(int a_NeedCookTime, int a_TimeCooked) {m_NeedCookTime = a_NeedCookTime; m_TimeCooked = a_TimeCooked; }
+
+protected:
+
+ /// Block type of the block currently represented by this entity (changes when furnace lights up)
+ BLOCKTYPE m_BlockType;
+
+ /// Block meta of the block currently represented by this entity
+ NIBBLETYPE m_BlockMeta;
+
+ /// The recipe for the current input slot
+ const cFurnaceRecipe::Recipe * m_CurrentRecipe;
+
+ /// The item that is being smelted
+ cItem m_LastInput;
+
+ bool m_IsCooking; ///< Set to true if the furnace is cooking an item
+
+ // All timers are in ticks
+ int m_NeedCookTime; ///< Amount of time needed to fully cook current item
+ int m_TimeCooked; ///< Amount of time that the current item has been cooking
+ int m_FuelBurnTime; ///< Amount of time that the current fuel can burn (in total); zero if no fuel burning
+ int m_TimeBurned; ///< Amount of time that the current fuel has been burning
+
+ int m_LastProgressFuel; ///< Last value sent as the progress for the fuel
+ int m_LastProgressCook; ///< Last value sent as the progress for the cooking
+
+
+ /// Sends the specified progressbar value to all clients of the window
+ void BroadcastProgress(int a_ProgressbarID, short a_Value);
+
+ /// One item finished cooking
+ void FinishOne(cChunk & a_Chunk);
+
+ /// Starts burning a new fuel, if possible
+ void BurnNewFuel(void);
+
+ /// Updates the recipe, based on the current input
+ void UpdateInput(void);
+
+ /// Called when the fuel slot changes or when the fuel is spent, burns another piece of fuel if appropriate
+ void UpdateFuel(void);
+
+ /// Called when the output slot changes
+ void UpdateOutput(void);
+
+ /// Updates the m_IsCooking, based on the input slot, output slot and m_FuelBurnTime / m_TimeBurned
+ void UpdateIsCooking(void);
+
+ /// Returns true if the input can be cooked into output and the item counts allow for another cooking operation
+ bool CanCookInputToOutput(void) const;
+
+ /// Broadcasts progressbar updates, if needed
+ void UpdateProgressBars(void);
+
+ /// Sets the m_IsCooking variable, updates the furnace block type based on the value
+ void SetIsCooking(bool a_IsCooking);
+
+ // cItemGrid::cListener overrides:
+ virtual void OnSlotChanged(cItemGrid * a_ItemGrid, int a_SlotNum) override;
+
+} ; // tolua_export
+
+
+
+
diff --git a/src/BlockEntities/HopperEntity.cpp b/src/BlockEntities/HopperEntity.cpp
new file mode 100644
index 000000000..41849b1b3
--- /dev/null
+++ b/src/BlockEntities/HopperEntity.cpp
@@ -0,0 +1,566 @@
+
+// HopperEntity.cpp
+
+// Implements the cHopperEntity representing a hopper block entity
+
+#include "Globals.h"
+#include "HopperEntity.h"
+#include "../Chunk.h"
+#include "../Entities/Player.h"
+#include "../PluginManager.h"
+#include "ChestEntity.h"
+#include "DropSpenserEntity.h"
+#include "FurnaceEntity.h"
+
+
+
+
+
+cHopperEntity::cHopperEntity(int a_BlockX, int a_BlockY, int a_BlockZ, cWorld * a_World) :
+ super(E_BLOCK_HOPPER, a_BlockX, a_BlockY, a_BlockZ, ContentsWidth, ContentsHeight, a_World),
+ m_LastMoveItemsInTick(0),
+ m_LastMoveItemsOutTick(0)
+{
+}
+
+
+
+
+
+/** Returns the block coords of the block receiving the output items, based on the meta
+Returns false if unattached
+*/
+bool cHopperEntity::GetOutputBlockPos(NIBBLETYPE a_BlockMeta, int & a_OutputX, int & a_OutputY, int & a_OutputZ)
+{
+ a_OutputX = m_PosX;
+ a_OutputY = m_PosY;
+ a_OutputZ = m_PosZ;
+ switch (a_BlockMeta)
+ {
+ case E_META_HOPPER_FACING_XM: a_OutputX--; return true;
+ case E_META_HOPPER_FACING_XP: a_OutputX++; return true;
+ case E_META_HOPPER_FACING_YM: a_OutputY--; return true;
+ case E_META_HOPPER_FACING_ZM: a_OutputZ--; return true;
+ case E_META_HOPPER_FACING_ZP: a_OutputZ++; return true;
+ default:
+ {
+ // Not attached
+ return false;
+ }
+ }
+}
+
+
+
+
+
+bool cHopperEntity::Tick(float a_Dt, cChunk & a_Chunk)
+{
+ Int64 CurrentTick = a_Chunk.GetWorld()->GetWorldAge();
+
+ bool res = false;
+ res = MoveItemsIn (a_Chunk, CurrentTick) || res;
+ res = MovePickupsIn(a_Chunk, CurrentTick) || res;
+ res = MoveItemsOut (a_Chunk, CurrentTick) || res;
+ return res;
+}
+
+
+
+
+
+void cHopperEntity::SaveToJson(Json::Value & a_Value)
+{
+ // TODO
+ LOGWARNING("%s: Not implemented yet", __FUNCTION__);
+}
+
+
+
+
+
+void cHopperEntity::SendTo(cClientHandle & a_Client)
+{
+ // The hopper entity doesn't need anything sent to the client when it's created / gets in the viewdistance
+ // All the actual handling is in the cWindow UI code that gets called when the hopper is rclked
+
+ UNUSED(a_Client);
+}
+
+
+
+
+
+void cHopperEntity::UsedBy(cPlayer * a_Player)
+{
+ // If the window is not created, open it anew:
+ cWindow * Window = GetWindow();
+ if (Window == NULL)
+ {
+ OpenNewWindow();
+ Window = GetWindow();
+ }
+
+ // Open the window for the player:
+ if (Window != NULL)
+ {
+ if (a_Player->GetWindow() != Window)
+ {
+ a_Player->OpenWindow(Window);
+ }
+ }
+
+ // This is rather a hack
+ // Instead of marking the chunk as dirty upon chest contents change, we mark it dirty now
+ // We cannot properly detect contents change, but such a change doesn't happen without a player opening the chest first.
+ // The few false positives aren't much to worry about
+ int ChunkX, ChunkZ;
+ cChunkDef::BlockToChunk(m_PosX, m_PosZ, ChunkX, ChunkZ);
+ m_World->MarkChunkDirty(ChunkX, ChunkZ);
+}
+
+
+
+
+
+/// Opens a new window UI for this hopper
+void cHopperEntity::OpenNewWindow(void)
+{
+ OpenWindow(new cHopperWindow(m_PosX, m_PosY, m_PosZ, this));
+}
+
+
+
+
+
+/// Moves items from the container above it into this hopper. Returns true if the contents have changed.
+bool cHopperEntity::MoveItemsIn(cChunk & a_Chunk, Int64 a_CurrentTick)
+{
+ if (m_PosY >= cChunkDef::Height)
+ {
+ // This hopper is at the top of the world, no more blocks above
+ return false;
+ }
+
+ if (a_CurrentTick - m_LastMoveItemsInTick < TICKS_PER_TRANSFER)
+ {
+ // Too early after the previous transfer
+ return false;
+ }
+
+ // Try moving an item in:
+ bool res = false;
+ switch (a_Chunk.GetBlock(m_RelX, m_PosY + 1, m_RelZ))
+ {
+ case E_BLOCK_CHEST:
+ {
+ // Chests have special handling because of double-chests
+ res = MoveItemsFromChest(a_Chunk);
+ break;
+ }
+ case E_BLOCK_LIT_FURNACE:
+ case E_BLOCK_FURNACE:
+ {
+ // Furnaces have special handling because only the output and leftover fuel buckets shall be moved
+ res = MoveItemsFromFurnace(a_Chunk);
+ break;
+ }
+ case E_BLOCK_DISPENSER:
+ case E_BLOCK_DROPPER:
+ case E_BLOCK_HOPPER:
+ {
+ res = MoveItemsFromGrid(*(cBlockEntityWithItems *)a_Chunk.GetBlockEntity(m_PosX, m_PosY + 1, m_PosZ));
+ break;
+ }
+ }
+
+ // If the item has been moved, reset the last tick:
+ if (res)
+ {
+ m_LastMoveItemsInTick = a_CurrentTick;
+ }
+
+ return res;
+}
+
+
+
+
+
+/// Moves pickups from above this hopper into it. Returns true if the contents have changed.
+bool cHopperEntity::MovePickupsIn(cChunk & a_Chunk, Int64 a_CurrentTick)
+{
+ // TODO
+ return false;
+}
+
+
+
+
+
+/// Moves items out from this hopper into the destination. Returns true if the contents have changed.
+bool cHopperEntity::MoveItemsOut(cChunk & a_Chunk, Int64 a_CurrentTick)
+{
+ if (a_CurrentTick - m_LastMoveItemsOutTick < TICKS_PER_TRANSFER)
+ {
+ // Too early after the previous transfer
+ return false;
+ }
+
+ int bx, by, bz;
+ NIBBLETYPE Meta = a_Chunk.GetMeta(m_RelX, m_PosY, m_RelZ);
+ if (!GetOutputBlockPos(Meta, bx, by, bz))
+ {
+ // Not attached to another container
+ return false;
+ }
+ if (by < 0)
+ {
+ // Cannot output below the zero-th block level
+ return false;
+ }
+
+ // Convert coords to relative:
+ int rx = bx - a_Chunk.GetPosX() * cChunkDef::Width;
+ int rz = bz - a_Chunk.GetPosZ() * cChunkDef::Width;
+ cChunk * DestChunk = a_Chunk.GetRelNeighborChunkAdjustCoords(rx, rz);
+ if (DestChunk == NULL)
+ {
+ // The destination chunk has been unloaded, don't tick
+ return false;
+ }
+
+ // Call proper moving function, based on the blocktype present at the coords:
+ bool res = false;
+ switch (DestChunk->GetBlock(rx, by, rz))
+ {
+ case E_BLOCK_CHEST:
+ {
+ // Chests have special handling because of double-chests
+ res = MoveItemsToChest(*DestChunk, bx, by, bz);
+ break;
+ }
+ case E_BLOCK_LIT_FURNACE:
+ case E_BLOCK_FURNACE:
+ {
+ // Furnaces have special handling because of the direction-to-slot relation
+ res = MoveItemsToFurnace(*DestChunk, bx, by, bz, Meta);
+ break;
+ }
+ case E_BLOCK_DISPENSER:
+ case E_BLOCK_DROPPER:
+ case E_BLOCK_HOPPER:
+ {
+ res = MoveItemsToGrid(*(cBlockEntityWithItems *)DestChunk->GetBlockEntity(bx, by, bz));
+ break;
+ }
+ }
+
+ // If the item has been moved, reset the last tick:
+ if (res)
+ {
+ m_LastMoveItemsOutTick = a_CurrentTick;
+ }
+
+ return res;
+}
+
+
+
+
+
+/// Moves items from a chest (dblchest) above the hopper into this hopper. Returns true if contents have changed.
+bool cHopperEntity::MoveItemsFromChest(cChunk & a_Chunk)
+{
+ if (MoveItemsFromGrid(*(cChestEntity *)a_Chunk.GetBlockEntity(m_PosX, m_PosY + 1, m_PosZ)))
+ {
+ // Moved the item from the chest directly above the hopper
+ return true;
+ }
+
+ // Check if the chest is a double-chest, if so, try to move from there:
+ static const struct
+ {
+ int x, z;
+ }
+ Coords [] =
+ {
+ {1, 0},
+ {-1, 0},
+ {0, 1},
+ {0, -1},
+ } ;
+ for (int i = 0; i < ARRAYCOUNT(Coords); i++)
+ {
+ int x = m_RelX + Coords[i].x;
+ int z = m_RelZ + Coords[i].z;
+ cChunk * Neighbor = a_Chunk.GetRelNeighborChunkAdjustCoords(x, z);
+ if (
+ (Neighbor == NULL) ||
+ (Neighbor->GetBlock(x, m_PosY + 1, z) != E_BLOCK_CHEST)
+ )
+ {
+ continue;
+ }
+ if (MoveItemsFromGrid(*(cChestEntity *)Neighbor->GetBlockEntity(x, m_PosY, z)))
+ {
+ return true;
+ }
+ return false;
+ }
+
+ // The chest was single and nothing could be moved
+ return false;
+}
+
+
+
+
+
+/// Moves items from a furnace above the hopper into this hopper. Returns true if contents have changed.
+bool cHopperEntity::MoveItemsFromFurnace(cChunk & a_Chunk)
+{
+ cFurnaceEntity * Furnace = (cFurnaceEntity *)a_Chunk.GetBlockEntity(m_PosX, m_PosY + 1, m_PosZ);
+ ASSERT(Furnace != NULL);
+
+ // Try move from the output slot:
+ if (MoveItemsFromSlot(*Furnace, cFurnaceEntity::fsOutput, true))
+ {
+ cItem NewOutput(Furnace->GetOutputSlot());
+ Furnace->SetOutputSlot(NewOutput.AddCount(-1));
+ return true;
+ }
+
+ // No output moved, check if we can move an empty bucket out of the fuel slot:
+ if (Furnace->GetFuelSlot().m_ItemType == E_ITEM_BUCKET)
+ {
+ if (MoveItemsFromSlot(*Furnace, cFurnaceEntity::fsFuel, true))
+ {
+ Furnace->SetFuelSlot(cItem());
+ return true;
+ }
+ }
+
+ // Nothing can be moved
+ return false;
+}
+
+
+
+
+
+bool cHopperEntity::MoveItemsFromGrid(cBlockEntityWithItems & a_Entity)
+{
+ cItemGrid & Grid = a_Entity.GetContents();
+ int NumSlots = Grid.GetNumSlots();
+
+ // First try adding items of types already in the hopper:
+ for (int i = 0; i < NumSlots; i++)
+ {
+ if (Grid.IsSlotEmpty(i))
+ {
+ continue;
+ }
+ if (MoveItemsFromSlot(a_Entity, i, false))
+ {
+ Grid.ChangeSlotCount(i, -1);
+ return true;
+ }
+ }
+
+ // No already existing stack can be topped up, try again with allowing new stacks:
+ for (int i = 0; i < NumSlots; i++)
+ {
+ if (Grid.IsSlotEmpty(i))
+ {
+ continue;
+ }
+ if (MoveItemsFromSlot(a_Entity, i, true))
+ {
+ Grid.ChangeSlotCount(i, -1);
+ return true;
+ }
+ }
+ return false;
+}
+
+
+
+
+
+/// Moves one piece of the specified a_Entity's slot itemstack into this hopper. Returns true if contents have changed. Doesn't change the itemstack.
+bool cHopperEntity::MoveItemsFromSlot(cBlockEntityWithItems & a_Entity, int a_SlotNum, bool a_AllowNewStacks)
+{
+ cItem One(a_Entity.GetSlot(a_SlotNum).CopyOne());
+ for (int i = 0; i < ContentsWidth * ContentsHeight; i++)
+ {
+ if (m_Contents.IsSlotEmpty(i))
+ {
+ if (a_AllowNewStacks)
+ {
+ if (cPluginManager::Get()->CallHookHopperPullingItem(*m_World, *this, i, a_Entity, a_SlotNum))
+ {
+ // Plugin disagrees with the move
+ continue;
+ }
+ }
+ m_Contents.SetSlot(i, One);
+ return true;
+ }
+ else if (m_Contents.GetSlot(i).IsStackableWith(One))
+ {
+ if (cPluginManager::Get()->CallHookHopperPullingItem(*m_World, *this, i, a_Entity, a_SlotNum))
+ {
+ // Plugin disagrees with the move
+ continue;
+ }
+
+ m_Contents.ChangeSlotCount(i, 1);
+ return true;
+ }
+ }
+ return false;
+}
+
+
+
+
+
+/// Moves items to the chest at the specified coords. Returns true if contents have changed
+bool cHopperEntity::MoveItemsToChest(cChunk & a_Chunk, int a_BlockX, int a_BlockY, int a_BlockZ)
+{
+ // Try the chest directly connected to the hopper:
+ if (MoveItemsToGrid(*(cChestEntity *)a_Chunk.GetBlockEntity(a_BlockX, a_BlockY, a_BlockZ)))
+ {
+ return true;
+ }
+
+ // Check if the chest is a double-chest, if so, try to move into the other half:
+ static const struct
+ {
+ int x, z;
+ }
+ Coords [] =
+ {
+ {1, 0},
+ {-1, 0},
+ {0, 1},
+ {0, -1},
+ } ;
+ for (int i = 0; i < ARRAYCOUNT(Coords); i++)
+ {
+ int x = m_RelX + Coords[i].x;
+ int z = m_RelZ + Coords[i].z;
+ cChunk * Neighbor = a_Chunk.GetRelNeighborChunkAdjustCoords(x, z);
+ if (
+ (Neighbor == NULL) ||
+ (Neighbor->GetBlock(x, m_PosY + 1, z) != E_BLOCK_CHEST)
+ )
+ {
+ continue;
+ }
+ if (MoveItemsToGrid(*(cChestEntity *)Neighbor->GetBlockEntity(a_BlockX, a_BlockY, a_BlockZ)))
+ {
+ return true;
+ }
+ return false;
+ }
+
+ // The chest was single and nothing could be moved
+ return false;
+}
+
+
+
+
+
+/// Moves items to the furnace at the specified coords. Returns true if contents have changed
+bool cHopperEntity::MoveItemsToFurnace(cChunk & a_Chunk, int a_BlockX, int a_BlockY, int a_BlockZ, NIBBLETYPE a_HopperMeta)
+{
+ cFurnaceEntity * Furnace = (cFurnaceEntity *)a_Chunk.GetBlockEntity(a_BlockX, a_BlockY, a_BlockZ);
+ if (a_HopperMeta == E_META_HOPPER_FACING_YM)
+ {
+ // Feed the input slot of the furnace
+ return MoveItemsToSlot(*Furnace, cFurnaceEntity::fsInput);
+ }
+ else
+ {
+ // Feed the fuel slot of the furnace
+ return MoveItemsToSlot(*Furnace, cFurnaceEntity::fsFuel);
+ }
+ return false;
+}
+
+
+
+
+
+bool cHopperEntity::MoveItemsToGrid(cBlockEntityWithItems & a_Entity)
+{
+ // Iterate through our slots, try to move from each one:
+ int NumSlots = a_Entity.GetContents().GetNumSlots();
+ for (int i = 0; i < NumSlots; i++)
+ {
+ if (MoveItemsToSlot(a_Entity, i))
+ {
+ return true;
+ }
+ }
+ return false;
+}
+
+
+
+
+
+bool cHopperEntity::MoveItemsToSlot(cBlockEntityWithItems & a_Entity, int a_DstSlotNum)
+{
+ cItemGrid & Grid = a_Entity.GetContents();
+ if (Grid.IsSlotEmpty(a_DstSlotNum))
+ {
+ // The slot is empty, move the first non-empty slot from our contents:
+ for (int i = 0; i < ContentsWidth * ContentsHeight; i++)
+ {
+ if (!m_Contents.IsSlotEmpty(i))
+ {
+ if (cPluginManager::Get()->CallHookHopperPushingItem(*m_World, *this, i, a_Entity, a_DstSlotNum))
+ {
+ // A plugin disagrees with the move
+ continue;
+ }
+ Grid.SetSlot(a_DstSlotNum, m_Contents.GetSlot(i).CopyOne());
+ m_Contents.ChangeSlotCount(i, -1);
+ return true;
+ }
+ }
+ return false;
+ }
+ else
+ {
+ // The slot is taken, try to top it up:
+ const cItem & DestSlot = Grid.GetSlot(a_DstSlotNum);
+ if (DestSlot.IsFullStack())
+ {
+ return false;
+ }
+ for (int i = 0; i < ContentsWidth * ContentsHeight; i++)
+ {
+ if (m_Contents.GetSlot(i).IsStackableWith(DestSlot))
+ {
+ if (cPluginManager::Get()->CallHookHopperPushingItem(*m_World, *this, i, a_Entity, a_DstSlotNum))
+ {
+ // A plugin disagrees with the move
+ continue;
+ }
+ Grid.ChangeSlotCount(a_DstSlotNum, 1);
+ m_Contents.ChangeSlotCount(i, -1);
+ return true;
+ }
+ }
+ return false;
+ }
+}
+
+
+
+
diff --git a/src/BlockEntities/HopperEntity.h b/src/BlockEntities/HopperEntity.h
new file mode 100644
index 000000000..3eaa05b7c
--- /dev/null
+++ b/src/BlockEntities/HopperEntity.h
@@ -0,0 +1,96 @@
+
+// HopperEntity.h
+
+// Declares the cHopperEntity representing a hopper block entity
+
+
+
+
+
+#pragma once
+
+#include "BlockEntityWithItems.h"
+#include "../UI/WindowOwner.h"
+
+
+
+
+
+class cHopperEntity : // tolua_export
+ public cBlockEntityWindowOwner,
+ // tolua_begin
+ public cBlockEntityWithItems
+{
+ typedef cBlockEntityWithItems super;
+
+public:
+ enum {
+ ContentsHeight = 1,
+ ContentsWidth = 5,
+ TICKS_PER_TRANSFER = 8, ///< How many ticks at minimum between two item transfers to or from the hopper
+ } ;
+
+ // tolua_end
+
+ /// Constructor used for normal operation
+ cHopperEntity(int a_BlockX, int a_BlockY, int a_BlockZ, cWorld * a_World);
+
+ /** Returns the block coords of the block receiving the output items, based on the meta
+ Returns false if unattached.
+ Exported in ManualBindings.cpp
+ */
+ bool GetOutputBlockPos(NIBBLETYPE a_BlockMeta, int & a_OutputX, int & a_OutputY, int & a_OutputZ);
+
+ static const char * GetClassStatic(void) { return "cHopperEntity"; }
+
+protected:
+
+ Int64 m_LastMoveItemsInTick;
+ Int64 m_LastMoveItemsOutTick;
+
+ // cBlockEntity overrides:
+ virtual bool Tick(float a_Dt, cChunk & a_Chunk) override;
+ virtual void SaveToJson(Json::Value & a_Value) override;
+ virtual void SendTo(cClientHandle & a_Client) override;
+ virtual void UsedBy(cPlayer * a_Player) override;
+
+ /// Opens a new chest window for this chest. Scans for neighbors to open a double chest window, if appropriate.
+ void OpenNewWindow(void);
+
+ /// Moves items from the container above it into this hopper. Returns true if the contents have changed.
+ bool MoveItemsIn(cChunk & a_Chunk, Int64 a_CurrentTick);
+
+ /// Moves pickups from above this hopper into it. Returns true if the contents have changed.
+ bool MovePickupsIn(cChunk & a_Chunk, Int64 a_CurrentTick);
+
+ /// Moves items out from this hopper into the destination. Returns true if the contents have changed.
+ bool MoveItemsOut(cChunk & a_Chunk, Int64 a_CurrentTick);
+
+ /// Moves items from a chest (dblchest) above the hopper into this hopper. Returns true if contents have changed.
+ bool MoveItemsFromChest(cChunk & a_Chunk);
+
+ /// Moves items from a furnace above the hopper into this hopper. Returns true if contents have changed.
+ bool MoveItemsFromFurnace(cChunk & a_Chunk);
+
+ /// Moves items from the specified a_Entity's Contents into this hopper. Returns true if contents have changed.
+ bool MoveItemsFromGrid(cBlockEntityWithItems & a_Entity);
+
+ /// Moves one piece from the specified itemstack into this hopper. Returns true if contents have changed. Doesn't change the itemstack.
+ bool MoveItemsFromSlot(cBlockEntityWithItems & a_Entity, int a_SrcSlotNum, bool a_AllowNewStacks);
+
+ /// Moves items to the chest at the specified coords. Returns true if contents have changed
+ bool MoveItemsToChest(cChunk & a_Chunk, int a_BlockX, int a_BlockY, int a_BlockZ);
+
+ /// Moves items to the furnace at the specified coords. Returns true if contents have changed
+ bool MoveItemsToFurnace(cChunk & a_Chunk, int a_BlockX, int a_BlockY, int a_BlockZ, NIBBLETYPE a_HopperMeta);
+
+ /// Moves items to the specified ItemGrid. Returns true if contents have changed
+ bool MoveItemsToGrid(cBlockEntityWithItems & a_Entity);
+
+ /// Moves one piece to the specified entity's contents' slot. Returns true if contents have changed.
+ bool MoveItemsToSlot(cBlockEntityWithItems & a_Entity, int a_DstSlotNum);
+} ; // tolua_export
+
+
+
+
diff --git a/src/BlockEntities/JukeboxEntity.cpp b/src/BlockEntities/JukeboxEntity.cpp
new file mode 100644
index 000000000..33042179d
--- /dev/null
+++ b/src/BlockEntities/JukeboxEntity.cpp
@@ -0,0 +1,125 @@
+
+#include "Globals.h" // NOTE: MSVC stupidness requires this to be the same across all modules
+
+#include "JukeboxEntity.h"
+#include "../World.h"
+#include "json/json.h"
+
+
+
+
+
+cJukeboxEntity::cJukeboxEntity(int a_BlockX, int a_BlockY, int a_BlockZ, cWorld * a_World) :
+ super(E_BLOCK_JUKEBOX, a_BlockX, a_BlockY, a_BlockZ, a_World),
+ m_Record(0)
+{
+}
+
+
+
+
+
+cJukeboxEntity::~cJukeboxEntity()
+{
+ EjectRecord();
+}
+
+
+
+
+
+void cJukeboxEntity::UsedBy(cPlayer * a_Player)
+{
+ if (m_Record == 0)
+ {
+ const cItem & HeldItem = a_Player->GetEquippedItem();
+ if (HeldItem.m_ItemType >= 2256 && HeldItem.m_ItemType <= 2267)
+ {
+ m_Record = HeldItem.m_ItemType;
+ a_Player->GetInventory().RemoveOneEquippedItem();
+ PlayRecord();
+ }
+ }
+ else
+ {
+ EjectRecord();
+ }
+}
+
+
+
+
+
+void cJukeboxEntity::PlayRecord(void)
+{
+ m_World->BroadcastSoundParticleEffect(1005, m_PosX, m_PosY, m_PosZ, m_Record);
+}
+
+
+
+
+
+void cJukeboxEntity::EjectRecord(void)
+{
+ if ((m_Record < E_ITEM_FIRST_DISC) || (m_Record > E_ITEM_LAST_DISC))
+ {
+ // There's no record here
+ return;
+ }
+
+ cItems Drops;
+ Drops.push_back(cItem(m_Record, 1, 0));
+ m_World->SpawnItemPickups(Drops, m_PosX + 0.5, m_PosY + 1, m_PosZ + 0.5, 8);
+ m_World->BroadcastSoundParticleEffect(1005, m_PosX, m_PosY, m_PosZ, 0);
+ m_Record = 0;
+}
+
+
+
+
+
+int cJukeboxEntity::GetRecord(void)
+{
+ return m_Record;
+}
+
+
+
+
+
+void cJukeboxEntity::SetRecord(int a_Record)
+{
+ m_Record = a_Record;
+}
+
+
+
+
+
+bool cJukeboxEntity::LoadFromJson(const Json::Value & a_Value)
+{
+ m_PosX = a_Value.get("x", 0).asInt();
+ m_PosY = a_Value.get("y", 0).asInt();
+ m_PosZ = a_Value.get("z", 0).asInt();
+
+ m_Record = a_Value.get("Record", 0).asInt();
+
+ return true;
+}
+
+
+
+
+
+void cJukeboxEntity::SaveToJson(Json::Value & a_Value)
+{
+ a_Value["x"] = m_PosX;
+ a_Value["y"] = m_PosY;
+ a_Value["z"] = m_PosZ;
+
+ a_Value["Record"] = m_Record;
+}
+
+
+
+
diff --git a/src/BlockEntities/JukeboxEntity.h b/src/BlockEntities/JukeboxEntity.h
new file mode 100644
index 000000000..fcafdc479
--- /dev/null
+++ b/src/BlockEntities/JukeboxEntity.h
@@ -0,0 +1,56 @@
+
+#pragma once
+
+#include "BlockEntity.h"
+#include "../Entities/Player.h"
+
+
+
+
+
+namespace Json
+{
+ class Value;
+}
+
+
+
+
+
+// tolua_begin
+
+class cJukeboxEntity :
+ public cBlockEntity
+{
+ typedef cBlockEntity super;
+public:
+
+ // tolua_end
+
+ cJukeboxEntity(int a_BlockX, int a_BlockY, int a_BlockZ, cWorld * a_World);
+ virtual ~cJukeboxEntity();
+
+ bool LoadFromJson(const Json::Value & a_Value);
+ virtual void SaveToJson(Json::Value & a_Value) override;
+
+ // tolua_begin
+
+ int GetRecord(void);
+ void SetRecord(int a_Record);
+ void PlayRecord(void);
+
+ /// Ejects the currently held record as a pickup. Does nothing when no record inserted.
+ void EjectRecord(void);
+
+ // tolua_end
+
+ virtual void UsedBy(cPlayer * a_Player) override;
+ virtual void SendTo(cClientHandle & a_Client) override { };
+
+private:
+ int m_Record;
+} ; // tolua_end
+
+
+
+
diff --git a/src/BlockEntities/NoteEntity.cpp b/src/BlockEntities/NoteEntity.cpp
new file mode 100644
index 000000000..9a33ead62
--- /dev/null
+++ b/src/BlockEntities/NoteEntity.cpp
@@ -0,0 +1,154 @@
+
+#include "Globals.h" // NOTE: MSVC stupidness requires this to be the same across all modules
+
+#include "NoteEntity.h"
+#include "../World.h"
+#include "json/json.h"
+
+
+
+
+
+cNoteEntity::cNoteEntity(int a_BlockX, int a_BlockY, int a_BlockZ, cWorld * a_World) :
+ super(E_BLOCK_NOTE_BLOCK, a_BlockX, a_BlockY, a_BlockZ, a_World),
+ m_Pitch(0)
+{
+}
+
+
+
+
+
+void cNoteEntity::UsedBy(cPlayer * a_Player)
+{
+ IncrementPitch();
+ MakeSound();
+}
+
+
+
+
+
+void cNoteEntity::MakeSound(void)
+{
+ char instrument;
+ AString sampleName;
+
+ switch (m_World->GetBlock(m_PosX, m_PosY - 1, m_PosZ))
+ {
+ case E_BLOCK_PLANKS:
+ case E_BLOCK_LOG:
+ case E_BLOCK_NOTE_BLOCK:
+ {
+ // TODO: add other wood-based blocks if needed
+ instrument = E_INST_DOUBLE_BASS;
+ sampleName = "note.db";
+ break;
+ }
+
+ case E_BLOCK_SAND:
+ case E_BLOCK_GRAVEL:
+ case E_BLOCK_SOULSAND:
+ {
+ instrument = E_INST_SNARE_DRUM;
+ sampleName = "note.snare";
+ break;
+ }
+
+ case E_BLOCK_GLASS:
+ case E_BLOCK_GLASS_PANE:
+ case E_BLOCK_GLOWSTONE:
+ {
+ instrument = E_INST_CLICKS;
+ sampleName = "note.hat";
+ break;
+ }
+
+ case E_BLOCK_STONE:
+ case E_BLOCK_STONE_BRICKS:
+ case E_BLOCK_COBBLESTONE:
+ case E_BLOCK_OBSIDIAN:
+ case E_BLOCK_NETHERRACK:
+ case E_BLOCK_BRICK:
+ case E_BLOCK_NETHER_BRICK:
+ {
+ // TODO: add other stone-based blocks if needed
+ instrument = E_INST_BASS_DRUM;
+ sampleName = "note.bassattack";
+ break;
+ }
+
+ default:
+ {
+ instrument = E_INST_HARP_PIANO;
+ sampleName = "note.harp";
+ break;
+ }
+ }
+
+ m_World->BroadcastBlockAction(m_PosX, m_PosY, m_PosZ, instrument, m_Pitch, E_BLOCK_NOTE_BLOCK);
+
+ // TODO: instead of calculating the power function over and over, make a precalculated table - there's only 24 pitches after all
+ float calcPitch = pow(2.0f, ((float)m_Pitch - 12.0f) / 12.0f);
+ m_World->BroadcastSoundEffect(sampleName, m_PosX * 8, m_PosY * 8, m_PosZ * 8, 3.0f, calcPitch);
+}
+
+
+
+
+
+char cNoteEntity::GetPitch(void)
+{
+ return m_Pitch;
+}
+
+
+
+
+
+void cNoteEntity::SetPitch(char a_Pitch)
+{
+ m_Pitch = a_Pitch % 25;
+}
+
+
+
+
+
+void cNoteEntity::IncrementPitch(void)
+{
+ SetPitch(m_Pitch + 1);
+}
+
+
+
+
+
+bool cNoteEntity::LoadFromJson(const Json::Value & a_Value)
+{
+
+ m_PosX = a_Value.get("x", 0).asInt();
+ m_PosY = a_Value.get("y", 0).asInt();
+ m_PosZ = a_Value.get("z", 0).asInt();
+
+ m_Pitch = (char)a_Value.get("p", 0).asInt();
+
+ return true;
+}
+
+
+
+
+
+void cNoteEntity::SaveToJson(Json::Value & a_Value)
+{
+ a_Value["x"] = m_PosX;
+ a_Value["y"] = m_PosY;
+ a_Value["z"] = m_PosZ;
+
+ a_Value["p"] = m_Pitch;
+}
+
+
+
+
diff --git a/src/BlockEntities/NoteEntity.h b/src/BlockEntities/NoteEntity.h
new file mode 100644
index 000000000..e2d088f44
--- /dev/null
+++ b/src/BlockEntities/NoteEntity.h
@@ -0,0 +1,63 @@
+
+#pragma once
+
+#include "BlockEntity.h"
+
+
+namespace Json
+{
+ class Value;
+}
+
+
+
+
+
+enum ENUM_NOTE_INSTRUMENTS
+{
+ E_INST_HARP_PIANO = 0,
+ E_INST_DOUBLE_BASS = 1,
+ E_INST_SNARE_DRUM = 2,
+ E_INST_CLICKS = 3,
+ E_INST_BASS_DRUM = 4
+};
+
+
+
+
+
+// tolua_begin
+
+class cNoteEntity :
+ public cBlockEntity
+{
+ typedef cBlockEntity super;
+public:
+
+ // tolua_end
+
+ /// Creates a new note entity. a_World may be NULL
+ cNoteEntity(int a_X, int a_Y, int a_Z, cWorld * a_World);
+
+ bool LoadFromJson(const Json::Value & a_Value);
+ virtual void SaveToJson(Json::Value & a_Value) override;
+
+ // tolua_begin
+
+ char GetPitch(void);
+ void SetPitch(char a_Pitch);
+ void IncrementPitch(void);
+ void MakeSound(void);
+
+ // tolua_end
+
+ virtual void UsedBy(cPlayer * a_Player) override;
+ virtual void SendTo(cClientHandle & a_Client) override { };
+
+private:
+ char m_Pitch;
+} ; // tolua_export
+
+
+
+
diff --git a/src/BlockEntities/SignEntity.cpp b/src/BlockEntities/SignEntity.cpp
new file mode 100644
index 000000000..df8774377
--- /dev/null
+++ b/src/BlockEntities/SignEntity.cpp
@@ -0,0 +1,115 @@
+
+// SignEntity.cpp
+
+// Implements the cSignEntity class representing a single sign in the world
+
+#include "Globals.h" // NOTE: MSVC stupidness requires this to be the same across all modules
+#include "json/json.h"
+#include "SignEntity.h"
+#include "../Entities/Player.h"
+
+
+
+
+
+cSignEntity::cSignEntity(BLOCKTYPE a_BlockType, int a_X, int a_Y, int a_Z, cWorld * a_World) :
+ super(a_BlockType, a_X, a_Y, a_Z, a_World)
+{
+}
+
+
+
+
+
+// It don't do anything when 'used'
+void cSignEntity::UsedBy(cPlayer * a_Player)
+{
+ UNUSED(a_Player);
+}
+
+
+
+
+
+void cSignEntity::SetLines(const AString & a_Line1, const AString & a_Line2, const AString & a_Line3, const AString & a_Line4)
+{
+ m_Line[0] = a_Line1;
+ m_Line[1] = a_Line2;
+ m_Line[2] = a_Line3;
+ m_Line[3] = a_Line4;
+}
+
+
+
+
+
+void cSignEntity::SetLine(int a_Index, const AString & a_Line)
+{
+ if ((a_Index < 0) || (a_Index >= ARRAYCOUNT(m_Line)))
+ {
+ LOGWARNING("%s: setting a non-existent line %d (value \"%s\"", __FUNCTION__, a_Index, a_Line.c_str());
+ return;
+ }
+ m_Line[a_Index] = a_Line;
+}
+
+
+
+
+
+AString cSignEntity::GetLine(int a_Index) const
+{
+ if ((a_Index < 0) || (a_Index >= ARRAYCOUNT(m_Line)))
+ {
+ LOGWARNING("%s: requesting a non-existent line %d", __FUNCTION__, a_Index);
+ return "";
+ }
+ return m_Line[a_Index];
+}
+
+
+
+
+
+void cSignEntity::SendTo(cClientHandle & a_Client)
+{
+ a_Client.SendUpdateSign(m_PosX, m_PosY, m_PosZ, m_Line[0], m_Line[1], m_Line[2], m_Line[3]);
+}
+
+
+
+
+
+bool cSignEntity::LoadFromJson(const Json::Value & a_Value)
+{
+ m_PosX = a_Value.get("x", 0).asInt();
+ m_PosY = a_Value.get("y", 0).asInt();
+ m_PosZ = a_Value.get("z", 0).asInt();
+
+ m_Line[0] = a_Value.get("Line1", "").asString();
+ m_Line[1] = a_Value.get("Line2", "").asString();
+ m_Line[2] = a_Value.get("Line3", "").asString();
+ m_Line[3] = a_Value.get("Line4", "").asString();
+
+ return true;
+}
+
+
+
+
+
+void cSignEntity::SaveToJson(Json::Value & a_Value)
+{
+ a_Value["x"] = m_PosX;
+ a_Value["y"] = m_PosY;
+ a_Value["z"] = m_PosZ;
+
+ a_Value["Line1"] = m_Line[0];
+ a_Value["Line2"] = m_Line[1];
+ a_Value["Line3"] = m_Line[2];
+ a_Value["Line4"] = m_Line[3];
+}
+
+
+
+
diff --git a/src/BlockEntities/SignEntity.h b/src/BlockEntities/SignEntity.h
new file mode 100644
index 000000000..d998ff1e8
--- /dev/null
+++ b/src/BlockEntities/SignEntity.h
@@ -0,0 +1,67 @@
+
+// SignEntity.h
+
+// Declares the cSignEntity class representing a single sign in the world
+
+
+
+
+
+#pragma once
+
+#include "BlockEntity.h"
+
+
+
+
+
+namespace Json
+{
+ class Value;
+}
+
+
+
+
+
+// tolua_begin
+
+class cSignEntity :
+ public cBlockEntity
+{
+ typedef cBlockEntity super;
+
+public:
+
+ // tolua_end
+
+ /// Creates a new empty sign entity at the specified block coords and block type (wall or standing). a_World may be NULL
+ cSignEntity(BLOCKTYPE a_BlockType, int a_X, int a_Y, int a_Z, cWorld * a_World);
+
+ bool LoadFromJson( const Json::Value& a_Value );
+ virtual void SaveToJson(Json::Value& a_Value ) override;
+
+ // tolua_begin
+
+ /// Sets all the sign's lines
+ void SetLines(const AString & a_Line1, const AString & a_Line2, const AString & a_Line3, const AString & a_Line4);
+
+ /// Sets individual line (zero-based index)
+ void SetLine(int a_Index, const AString & a_Line);
+
+ /// Retrieves individual line (zero-based index)
+ AString GetLine(int a_Index) const;
+
+ // tolua_end
+
+ virtual void UsedBy(cPlayer * a_Player) override;
+ virtual void SendTo(cClientHandle & a_Client) override;
+
+private:
+
+ AString m_Line[4];
+} ; // tolua_export
+
+
+
+