diff --git a/game/assets/.gitignore b/game/assets/.gitignore index 07ac24e4b..4127933db 100644 --- a/game/assets/.gitignore +++ b/game/assets/.gitignore @@ -101,3 +101,4 @@ !/patches/p_info_updategame1.png !/patches/p_info_updategame2.png !/patches/p_slot_more.png +/resource_packs/ diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt index 5c0ab5a1a..751511004 100644 --- a/source/CMakeLists.txt +++ b/source/CMakeLists.txt @@ -533,6 +533,16 @@ add_library(nbcraft-core STATIC renderer/hal/interface/ShaderConstantWithData.cpp renderer/hal/interface/ShaderProgram.cpp renderer/hal/interface/Texture.cpp + world/tile/FurnaceTile.cpp + world/tile/FurnaceTile.hpp + client/gui/screens/inventory/FurnaceScreen.cpp + client/gui/screens/inventory/FurnaceScreen.hpp + world/inventory/FurnaceMenu.cpp + world/inventory/FurnaceMenu.h + world/inventory/FurnaceTileEntity.cpp + world/inventory/FurnaceTileEntity.h + world/level/tileentity/TileEntity.cpp + world/level/tileentity/TileEntity.h ) target_include_directories(nbcraft-core PUBLIC . ..) diff --git a/source/client/gui/screens/inventory/FurnaceScreen.cpp b/source/client/gui/screens/inventory/FurnaceScreen.cpp new file mode 100644 index 000000000..71d9303cc --- /dev/null +++ b/source/client/gui/screens/inventory/FurnaceScreen.cpp @@ -0,0 +1,73 @@ +#include "FurnaceScreen.hpp" +#include "world/inventory/FurnaceMenu.h" +#include "client/app/Minecraft.hpp" + +FurnaceScreen::FurnaceScreen(Inventory* inventory, FurnaceTileEntity* furnace) + : ContainerScreen(new FurnaceMenu(inventory, furnace)), m_pFurnace(furnace) +{ +} + +void FurnaceScreen::_renderLabels() { + // Draw text using the built-in font renderer (Text, X, Y, Hex Color) + m_pFont->draw("Furnace", 64 , 6, 0x404040); +} + +void FurnaceScreen::_renderBg(float partialTick) { + // 1. Bind the texture file (using the exact pointer your compiler suggested) + m_pMinecraft->m_pTextures->loadAndBindTexture("gui/furnace.png"); + + // 2. Define the exact pixel size of the texture locally + int xSize = 176; + int ySize = 166; + + // 3. Center the UI on the screen + int x = (m_width - xSize) / 2; + int y = (m_height - ySize) / 2; + + // 4. Draw the main background + // Notice how we pass xSize and ySize twice (Destination Width/Height, then Source Width/Height) + blit(x, y, 0, 0, xSize, ySize, xSize, ySize); + + // 5. Draw the animated Fire (Fuel) + if (m_pFurnace->isBurning()) { + int fireHeight = 12; // Maximum height of the fire icon + if (m_pFurnace->currentItemBurnTime > 0) { + // Scale the fire down as the fuel burns away + fireHeight = (m_pFurnace->furnaceBurnTime * 12) / m_pFurnace->currentItemBurnTime; + } + + // Draw the fire! (Again, we duplicate the width '14' and height 'fireHeight + 2') + blit(x + 56, y + 36 + 12 - fireHeight, 176, 12 - fireHeight, 14, fireHeight + 2, 14, fireHeight + 2); + } + + // 6. Draw the animated Progress Arrow (Cook Time) + // The arrow is 24 pixels wide, and a full smelt takes 200 ticks + int arrowWidth = (m_pFurnace->furnaceCookTime * 24) / 200; + + // Draw the arrow! (Duplicating the width 'arrowWidth' and height '16') + blit(x + 79, y + 34, 176, 14, arrowWidth, 16, arrowWidth, 16); +} + +SlotDisplay FurnaceScreen::_createSlotDisplay(const Slot& slot) { + + if (slot.m_pContainer == m_pFurnace) { + const int id = slot.m_slot; // Slot 0, 1, or 2 + if (id == 0) return SlotDisplay(56, 17); // Top Input Slot + if (id == 1) return SlotDisplay(56, 53); // Bottom Fuel Slot + if (id == 2) return SlotDisplay(116, 35); // Right Output Slot + } + // Otherwise, it belongs to the Player's Inventory + else { + const int id = slot.m_slot; + if (id < 9) { + // The Hotbar (Bottom row) + return SlotDisplay(8 + id * 18, 142); + } else { + // The Main Backpack (3 rows of 9) + int gridX = (id - 9) % 9; + int gridY = (id - 9) / 9; + return SlotDisplay(8 + gridX * 18, 84 + gridY * 18); + } + } + return SlotDisplay(0, 0); // Safety fallback +} \ No newline at end of file diff --git a/source/client/gui/screens/inventory/FurnaceScreen.hpp b/source/client/gui/screens/inventory/FurnaceScreen.hpp new file mode 100644 index 000000000..d7ead8ef8 --- /dev/null +++ b/source/client/gui/screens/inventory/FurnaceScreen.hpp @@ -0,0 +1,24 @@ +#ifndef NBCRAFT_FURNACESCREEN_H +#define NBCRAFT_FURNACESCREEN_H + +#include "ContainerScreen.hpp" +#include "world/inventory/FurnaceTileEntity.h" + +class FurnaceScreen : public ContainerScreen { +private: + FurnaceTileEntity* m_pFurnace; + +public: + FurnaceScreen(Inventory* inventory, FurnaceTileEntity* furnace); + + // Fixed the C++11 warning here! + ~FurnaceScreen() override {} + +protected: + // These are the exact 3 pure virtual methods NBCraft demands! + void _renderLabels() override; + void _renderBg(float partialTick) override; + SlotDisplay _createSlotDisplay(const Slot& slot) override; +}; + +#endif // NBCRAFT_FURNACESCREEN_H \ No newline at end of file diff --git a/source/client/player/LocalPlayer.cpp b/source/client/player/LocalPlayer.cpp index a328dbe5c..fef1f0f56 100644 --- a/source/client/player/LocalPlayer.cpp +++ b/source/client/player/LocalPlayer.cpp @@ -13,6 +13,8 @@ #include "network/packets/PlayerEquipmentPacket.hpp" #include "client/gui/screens/inventory/CraftingScreen.hpp" #include "client/gui/screens/inventory/ChestScreen.hpp" +#include "client/gui/screens/inventory/FurnaceScreen.hpp" +#include "world/inventory/FurnaceTileEntity.h" int dword_250ADC, dword_250AE0; @@ -134,11 +136,11 @@ void LocalPlayer::startCrafting(const TilePos& pos) m_pMinecraft->getScreenChooser()->pushCraftingScreen(this, pos); } -/*void LocalPlayer::openFurnace(FurnaceTileEntity* furnace) +void LocalPlayer::openFurnace(FurnaceTileEntity* furnace) { // PE 0.3.2 doesn't let you cook in creative mode m_pMinecraft->setScreen(new FurnaceScreen(m_pInventory, furnace)); -}*/ +} void LocalPlayer::openContainer(Container* container) { diff --git a/source/client/player/LocalPlayer.hpp b/source/client/player/LocalPlayer.hpp index 1eecb1288..6781b193a 100644 --- a/source/client/player/LocalPlayer.hpp +++ b/source/client/player/LocalPlayer.hpp @@ -11,6 +11,7 @@ #include "world/entity/Player.hpp" #include "client/player/input/IMoveInput.hpp" #include "client/player/input/User.hpp" +#include "world/inventory/FurnaceTileEntity.h" class Minecraft; @@ -39,6 +40,8 @@ class LocalPlayer : public Player void setPlayerGameType(GameType gameType) override; void swing() override; void startCrafting(const TilePos&) override; + void openFurnace(FurnaceTileEntity *furnace); + //void openFurnace(FurnaceTileEntity* furnace) override; void openContainer(Container* container) override; void closeContainer() override; diff --git a/source/world/entity/Player.cpp b/source/world/entity/Player.cpp index cbeef3ab1..aa45962bb 100644 --- a/source/world/entity/Player.cpp +++ b/source/world/entity/Player.cpp @@ -557,9 +557,16 @@ void Player::drop(const ItemStack& item, bool randomly) reallyDrop(pItemEntity); } -void Player::startCrafting(const TilePos& pos) -{ +void Player::startCrafting(const TilePos& pos) { +} +void Player::openFurnace(const TilePos& pos) +{ + TileEntity* te = m_pLevel->getTileEntity(pos.x, pos.y, pos.z); + if (te != nullptr) + { + this->openFurnace(static_cast(te)); + } } void Player::startStonecutting(const TilePos& pos) diff --git a/source/world/entity/Player.hpp b/source/world/entity/Player.hpp index c75ff8831..5e87c51c8 100644 --- a/source/world/entity/Player.hpp +++ b/source/world/entity/Player.hpp @@ -13,6 +13,7 @@ #include "world/entity/Mob.hpp" #include "world/entity/ItemEntity.hpp" #include "world/gamemode/GameType.hpp" +#include "world/inventory/FurnaceTileEntity.h" #include "world/inventory/InventoryMenu.hpp" #define C_PLAYER_FLAG_USING_ITEM (4) @@ -69,6 +70,12 @@ class Player : public Mob //virtual void drop(); // see definition virtual void drop(const ItemStack& item, bool randomly = false); virtual void startCrafting(const TilePos& pos); + + void openFurnace(const TilePos &pos); + virtual void openFurnace(FurnaceTileEntity* furnace) {} + + void startSmelting(const TilePos &pos); + virtual void startStonecutting(const TilePos& pos); virtual void startDestroying(); virtual void stopDestroying(); diff --git a/source/world/inventory/FurnaceMenu.cpp b/source/world/inventory/FurnaceMenu.cpp new file mode 100644 index 000000000..c88468bbe --- /dev/null +++ b/source/world/inventory/FurnaceMenu.cpp @@ -0,0 +1,40 @@ +#include "FurnaceMenu.h" + +#include "FurnaceTileEntity.h" +#include "world/inventory/ResultSlot.hpp" +#include "world/inventory/Slot.hpp" + + +FurnaceMenu::FurnaceMenu(Inventory* inventory, FurnaceTileEntity* furnace) + : ContainerMenu(Container::FURNACE) + , m_pFurnace(furnace) +{ + // Slot 0: Input (Top slot of the furnace) + // Slot 1: Fuel (Bottom slot of the furnace) + // Slot 2: Output/Result (Right slot of the furnace) + + // Note: The actual coordinates (x, y) for drawing the slots will be handled by FurnaceScreen later. + // For now, we just register them in order. + + addSlot(new Slot(m_pFurnace, 0, Slot::INPUT)); // Input + addSlot(new Slot(m_pFurnace, 1, Slot::INPUT)); // Fuel + addSlot(new ResultSlot(inventory->m_pPlayer, nullptr, m_pFurnace, 2)); // Result + + // Add the 27 slots for the player's main inventory + for (int y = 0; y < 3; ++y) { + for (int x = 0; x < 9; ++x) { + addSlot(new Slot(inventory, x + y * 9 + 9, Slot::INVENTORY)); + } + } + + // Add the 9 slots for the player's hotbar + for (int i = 0; i < 9; ++i) { + addSlot(new Slot(inventory, i, Slot::HOTBAR)); + } +} + +bool FurnaceMenu::stillValid(Player* player) const +{ + // The FurnaceTileEntity will tell us if it's still valid (e.g., if the block was destroyed) + return m_pFurnace->stillValid(player); +} \ No newline at end of file diff --git a/source/world/inventory/FurnaceMenu.h b/source/world/inventory/FurnaceMenu.h new file mode 100644 index 000000000..013938552 --- /dev/null +++ b/source/world/inventory/FurnaceMenu.h @@ -0,0 +1,25 @@ +#ifndef NBCRAFT_FURNACEMENU_H +#define NBCRAFT_FURNACEMENU_H + +#include "world/inventory/ContainerMenu.hpp" +#include "world/item/Inventory.hpp" + +// Forward declaration so the compiler knows this class exists +class FurnaceTileEntity; + +class FurnaceMenu : public ContainerMenu { +public: + FurnaceMenu(Inventory* inventory, FurnaceTileEntity* furnace); + ~FurnaceMenu() override = default; + + bool stillValid(Player* player) const override; + // slotsChanged is NOT needed for a furnace! The TileEntity handles smelting. + + // quickMoveStack is complex, we can leave it empty for now to prevent crashes + ItemStack quickMoveStack(int index) override { return ItemStack::EMPTY; } + +private: + FurnaceTileEntity *m_pFurnace; +}; + +#endif //NBCRAFT_FURNACEMENU_H \ No newline at end of file diff --git a/source/world/inventory/FurnaceTileEntity.cpp b/source/world/inventory/FurnaceTileEntity.cpp new file mode 100644 index 000000000..b4c35c845 --- /dev/null +++ b/source/world/inventory/FurnaceTileEntity.cpp @@ -0,0 +1,167 @@ +#include "FurnaceTileEntity.h" +#include "world/item/crafting/FurnaceRecipes.hpp" +#include "world/item/Item.hpp" +#include "world/item/TileItem.hpp" +#include "world/tile/FurnaceTile.hpp" +#include "world/level/Level.hpp" +#include "common/Utils.hpp" + +void FurnaceTileEntity::save(CompoundTag& tag) const { + TileEntity::save(tag); // writes id, x, y, z + tag.putInt32("BurnTime", furnaceBurnTime); + tag.putInt32("CookTime", furnaceCookTime); + tag.putInt32("BurnTimeTotal", currentItemBurnTime); + + ListTag* items = new ListTag(); + for (int i = 0; i < 3; i++) { + if (!m_items[i].isEmpty()) { + CompoundTag* slot = new CompoundTag(); + slot->putInt8("Slot", (int8_t)i); + m_items[i].save(*slot); + items->add(slot); + } + } + tag.put("Items", items); +} + +void FurnaceTileEntity::load(const CompoundTag& tag) { + TileEntity::load(tag); // reads x, y, z + furnaceBurnTime = tag.getInt32("BurnTime"); + furnaceCookTime = tag.getInt32("CookTime"); + currentItemBurnTime = tag.getInt32("BurnTimeTotal"); + + const ListTag* items = tag.getList("Items"); + if (items) { + for (unsigned int i = 0; i < items->rawView().size(); i++) { + const CompoundTag* slot = items->getCompound(i); + if (!slot) continue; + int slotIdx = (uint8_t)slot->getInt8("Slot"); + if (slotIdx >= 0 && slotIdx < 3) + m_items[slotIdx] = ItemStack::fromTag(*slot); + } + } +} + +uint16_t FurnaceTileEntity::getContainerSize() const { return 3; } + +ItemStack& FurnaceTileEntity::getItem(int index) { return m_items[index]; } + +void FurnaceTileEntity::setItem(int index, const ItemStack& item) { + m_items[index] = item; + if (item.m_count > getMaxStackSize()) { + m_items[index].m_count = getMaxStackSize(); + } + setChanged(); +} + +ItemStack FurnaceTileEntity::removeItem(int index, int count) { + if (!m_items[index].isEmpty()) { + if (m_items[index].m_count <= count) { + ItemStack result = m_items[index]; + m_items[index] = ItemStack::EMPTY; + setChanged(); + return result; + } + ItemStack result = m_items[index]; + result.m_count = count; + m_items[index].m_count -= count; + setChanged(); + return result; + } + return ItemStack::EMPTY; +} + +std::string FurnaceTileEntity::getName() const { return "Furnace"; } + +int FurnaceTileEntity::getMaxStackSize() { return 64; } + +void FurnaceTileEntity::setChanged() { markDirty(); } + +bool FurnaceTileEntity::stillValid(Player* player) const { return true; } + +bool FurnaceTileEntity::isBurning() const { return furnaceBurnTime > 0; } + +void FurnaceTileEntity::tick() { + bool wasBurning = isBurning(); + bool needsUpdate = false; + + // 1. Burn fuel + if (furnaceBurnTime > 0) { + furnaceBurnTime--; + } + + // 2. Check if we need to consume more fuel to keep smelting + if (furnaceBurnTime == 0 && canSmelt()) { + currentItemBurnTime = furnaceBurnTime = getBurnDuration(m_items[1]); + if (furnaceBurnTime > 0) { + needsUpdate = true; + if (!m_items[1].isEmpty()) { + m_items[1].m_count--; + if (m_items[1].m_count == 0) m_items[1] = ItemStack::EMPTY; + } + } + } + + // 3. Process the smelting item + if (isBurning() && canSmelt()) { + furnaceCookTime++; + if (furnaceCookTime == 200) { // 200 ticks = 10 seconds per item + furnaceCookTime = 0; + smeltItem(); + needsUpdate = true; + } + } else { + furnaceCookTime = 0; + } + + if (wasBurning != isBurning()) { + TileData currentData = level->getData(pos); + FurnaceTile::s_swappingLitState = true; + level->setTileAndData(pos, isBurning() ? TILE_FURNACE_LIT : TILE_FURNACE, currentData); + FurnaceTile::s_swappingLitState = false; + needsUpdate = true; + } + + if (needsUpdate) { + setChanged(); + } +} + +bool FurnaceTileEntity::canSmelt() { + if (m_items[0].isEmpty()) return false; + + // We pass 'this' because the recipe manager wants the whole container to check slot 0 + ItemStack result = FurnaceRecipes::singleton().getItemFor(this); + if (result.isEmpty()) return false; + + // Check if output slot is empty, matches the result type, and has room + if (m_items[2].isEmpty()) return true; + + // We use getId() instead of trying to access a private variable + if (m_items[2].getId() != result.getId()) return false; + + if (m_items[2].m_count < getMaxStackSize() && m_items[2].m_count < m_items[2].getMaxStackSize()) return true; + + return m_items[2].m_count < result.getMaxStackSize(); +} + +void FurnaceTileEntity::smeltItem() { + if (!canSmelt()) return; + + ItemStack result = FurnaceRecipes::singleton().getItemFor(this); + + if (m_items[2].isEmpty()) { + m_items[2] = result; + } else if (m_items[2].getId() == result.getId()) { + m_items[2].m_count += result.m_count; // Add to existing stack + } + + // Consume the raw material + m_items[0].m_count--; + if (m_items[0].m_count <= 0) m_items[0] = ItemStack::EMPTY; +} + +int FurnaceTileEntity::getBurnDuration(const ItemStack& item) { + // We completely delete the hardcoded items and just ask the recipe manager! + return FurnaceRecipes::singleton().getBurnDuration(item); +} \ No newline at end of file diff --git a/source/world/inventory/FurnaceTileEntity.h b/source/world/inventory/FurnaceTileEntity.h new file mode 100644 index 000000000..618872b66 --- /dev/null +++ b/source/world/inventory/FurnaceTileEntity.h @@ -0,0 +1,51 @@ +// FurnaceTileEntity.h +#ifndef NBCRAFT_FURNACETILEENTITY_H +#define NBCRAFT_FURNACETILEENTITY_H + +#include "world/Container.hpp" +#include "world/item/ItemStack.hpp" +#include "world/level/tileentity/TileEntity.h" + +class FurnaceTileEntity : public TileEntity, public Container { +private: + ItemStack m_items[3]; + +public: + int furnaceBurnTime; + int currentItemBurnTime; + int furnaceCookTime; + + FurnaceTileEntity(const TilePos& pos) + : TileEntity("Furnace", pos), + furnaceBurnTime(0), currentItemBurnTime(0), furnaceCookTime(0) + { + m_items[0] = ItemStack::EMPTY; + m_items[1] = ItemStack::EMPTY; + m_items[2] = ItemStack::EMPTY; + } + + ~FurnaceTileEntity() {} + + // --- Container Overrides --- + uint16_t getContainerSize() const override; + ItemStack& getItem(int index) override; + void setItem(int index, const ItemStack& item) override; + ItemStack removeItem(int index, int count) override; + std::string getName() const override; + int getMaxStackSize() override; + void setChanged() override; + bool stillValid(Player* player) const override; + + // --- TileEntity Overrides --- + void save(CompoundTag& tag) const override; + void load(const CompoundTag& tag) override; + + // --- Furnace Logic --- + void tick(); + bool isBurning() const; + bool canSmelt(); + void smeltItem(); + int getBurnDuration(const ItemStack& item); +}; + +#endif // NBCRAFT_FURNACETILEENTITY_H \ No newline at end of file diff --git a/source/world/inventory/ResultSlot.cpp b/source/world/inventory/ResultSlot.cpp index 1a8a98a55..3db5f3ca9 100644 --- a/source/world/inventory/ResultSlot.cpp +++ b/source/world/inventory/ResultSlot.cpp @@ -41,16 +41,18 @@ void ResultSlot::onTake(ItemStack& item) //else if (item.getId() == Item::woodSword->getId()) // m_pPlayer->awardStat(Achievements::buildSword); - for (int i = 0; i < m_pCraftSlots->getContainerSize(); ++i) - { - ItemStack& containerItem = m_pCraftSlots->getItem(i); - if (!containerItem.isEmpty()) - { - Item* item = containerItem.getItem(); - m_pCraftSlots->removeItem(i, 1); + if (m_pCraftSlots) { + for (int i = 0; i < m_pCraftSlots->getContainerSize(); ++i) + { + ItemStack& containerItem = m_pCraftSlots->getItem(i); + if (!containerItem.isEmpty()) + { + Item* item = containerItem.getItem(); + m_pCraftSlots->removeItem(i, 1); - if (item->hasCraftingRemainingItem()) - m_pCraftSlots->setItem(i, ItemStack(item->getCraftingRemainingItem())); + if (item->hasCraftingRemainingItem()) + m_pCraftSlots->setItem(i, ItemStack(item->getCraftingRemainingItem())); + } } } } diff --git a/source/world/item/crafting/FurnaceRecipes.cpp b/source/world/item/crafting/FurnaceRecipes.cpp index 0c170f30d..2b9bcb36b 100644 --- a/source/world/item/crafting/FurnaceRecipes.cpp +++ b/source/world/item/crafting/FurnaceRecipes.cpp @@ -1,6 +1,8 @@ #include "FurnaceRecipes.hpp" #include "common/Logger.hpp" +FurnaceRecipes* FurnaceRecipes::instance = NULL; + FurnaceRecipes::FurnaceRecipes() { addFurnaceRecipe(Tile::ironOre, ItemStack(Item::ironIngot)); diff --git a/source/world/item/crafting/Recipes.cpp b/source/world/item/crafting/Recipes.cpp index 05f0e32c0..c600f968d 100644 --- a/source/world/item/crafting/Recipes.cpp +++ b/source/world/item/crafting/Recipes.cpp @@ -124,10 +124,10 @@ Recipes::Recipes() // "# #", // "###", ItemStack(Tile::chest)) // .add('#', Tile::wood)); - //add(ShapedRecipeBuilder("###", - // "# #", - // "###", ItemStack(Tile::furnace)) - // .add('#', Tile::stoneBrick)); + add(ShapedRecipeBuilder("###", + "# #", + "###", ItemStack(Tile::furnace)) + .add('#', Tile::stoneBrick)); add(ShapedRecipeBuilder("##", "##", ItemStack(Tile::craftingTable)) diff --git a/source/world/level/Level.cpp b/source/world/level/Level.cpp index c67d40524..0067e8ad4 100644 --- a/source/world/level/Level.cpp +++ b/source/world/level/Level.cpp @@ -1607,6 +1607,8 @@ void Level::tickTiles() if (Tile::shouldTick[tile]) Tile::tiles[tile]->tick(this, tilePos + pos, &m_random); } + + pChunk->tickTileEntities(); } } @@ -2029,3 +2031,20 @@ float Level::getSunAngle(float f) const { return (float(M_PI) * getTimeOfDay(f)) * 2; } + +void Level::setTileEntity(int x, int y, int z, TileEntity* te) { + LevelChunk* chunk = getChunkAt(TilePos(x, y, z)); + if (chunk) chunk->addTileEntity(te); +} + +TileEntity* Level::getTileEntity(int x, int y, int z) { + LevelChunk* chunk = getChunkAt(TilePos(x, y, z)); + if (!chunk) return nullptr; + return chunk->getTileEntity(ChunkTilePos(TilePos(x, y, z))); +} + +void Level::removeTileEntity(int x, int y, int z) { + LevelChunk* chunk = getChunkAt(TilePos(x, y, z)); + if (chunk) chunk->removeTileEntity(TilePos(x, y, z)); +} + diff --git a/source/world/level/Level.hpp b/source/world/level/Level.hpp index 993830cf6..8b55b8c1b 100644 --- a/source/world/level/Level.hpp +++ b/source/world/level/Level.hpp @@ -15,6 +15,7 @@ #endif #include +#include "world/level/TilePos.hpp" #include "client/renderer/LightUpdate.hpp" #include "world/tile/Tile.hpp" #include "world/entity/Entity.hpp" @@ -197,13 +198,16 @@ class Level : public LevelSource bool hasDirectSignal(const TilePos& pos) const; bool hasNeighborSignal(const TilePos& pos) const; + void setTileEntity(int x, int y, int z, TileEntity* te); + TileEntity* getTileEntity(int x, int y, int z); + void removeTileEntity(int x, int y, int z); + #ifdef ENH_IMPROVED_SAVING void saveUnsavedChunks(); #endif private: LevelData* m_pLevelData; - protected: int m_randValue; int m_addend; diff --git a/source/world/level/levelgen/chunk/LevelChunk.cpp b/source/world/level/levelgen/chunk/LevelChunk.cpp index 3e36167dd..d59ba1010 100644 --- a/source/world/level/levelgen/chunk/LevelChunk.cpp +++ b/source/world/level/levelgen/chunk/LevelChunk.cpp @@ -9,16 +9,22 @@ #include "common/Logger.hpp" #include "world/level/Level.hpp" #include "world/phys/AABB.hpp" +#include "world/tile/Tile.hpp" bool LevelChunk::touchedSky = false; LevelChunk::~LevelChunk() { + // Clean up Tile Entities + for (std::map::iterator it = m_tileEntities.begin(); it != m_tileEntities.end(); ++it) { + delete it->second; + } + m_tileEntities.clear(); + SAFE_DELETE_ARRAY(m_lightBlk.m_data); SAFE_DELETE_ARRAY(m_lightSky.m_data); SAFE_DELETE_ARRAY(m_tileData.m_data); } - CONSTEXPR int MakeBlockDataIndex(const ChunkTilePos& pos) { return (pos.x << 11) | (pos.z << 7) | pos.y; @@ -627,36 +633,27 @@ bool LevelChunk::setTileAndData(const ChunkTilePos& pos, TileID tile, TileData d CheckPosition(pos); int index = MakeBlockDataIndex(pos); - TileID oldTile = m_pBlockData[index]; - uint8_t height = m_heightMap[MakeHeightMapIndex(pos)]; - if (oldTile == tile) - { - // make sure we're at least updating the data. If not, simply return false - if (getData(pos) == data) - return false; - } + if (oldTile == tile && getData(pos) == data) + return false; TilePos tilePos(m_chunkPos, pos.y); tilePos.x += pos.x; tilePos.z += pos.z; + m_pBlockData[index] = tile; + if (oldTile && Tile::tiles[oldTile]) { + if (Tile::isEntityTile[oldTile]) + removeTileEntity(tilePos); Tile::tiles[oldTile]->onRemove(m_pLevel, tilePos); } - // update the data value of the block m_tileData.set(pos, data); - if (m_pLevel->m_pDimension->m_bHasCeiling) - { - m_pLevel->updateLight(LightLayer::Block, tilePos, tilePos); - lightGaps(pos); - } - if (Tile::lightBlock[tile]) { if (height <= pos.y) @@ -671,10 +668,15 @@ bool LevelChunk::setTileAndData(const ChunkTilePos& pos, TileID tile, TileData d m_pLevel->updateLight(LightLayer::Block, tilePos, tilePos); lightGaps(pos); - if (tile) + + if (tile && !m_pLevel->m_bIsClientSide) { - if (!m_pLevel->m_bIsClientSide) - Tile::tiles[tile]->onPlace(m_pLevel, tilePos); + Tile::tiles[tile]->onPlace(m_pLevel, tilePos); + if (Tile::isEntityTile[tile]) + { + TileEntity* te = Tile::tiles[tile]->createTileEntity(tilePos); + if (te) addTileEntity(te); + } } m_bUnsaved = true; @@ -918,3 +920,77 @@ bool LevelChunk::isEmpty() { return false; } + +void LevelChunk::addTileEntity(TileEntity* te) { + // Stick to TilePos for the map key to match Level.cpp logic + TilePos localPos(te->pos.x, te->pos.y, te->pos.z); + + removeTileEntity(localPos); + m_tileEntities[localPos] = te; + te->level = m_pLevel; +} + +void LevelChunk::removeTileEntity(const TilePos& pos) { + if (m_tileEntities.find(pos) != m_tileEntities.end()) { + delete m_tileEntities[pos]; + m_tileEntities.erase(pos); + } +} + +void LevelChunk::tickTileEntities() { + // Ensure the iterator type matches the map declaration exactly + for (std::map::iterator it = m_tileEntities.begin(); it != m_tileEntities.end(); ++it) { + it->second->tick(); + } +} + +void LevelChunk::loadTileEntities(const ListTag& list) +{ + const std::vector& tags = list.rawView(); + + for (unsigned int i = 0; i < tags.size(); i++) + { + const CompoundTag* teTag = list.getCompound(i); + if (!teTag) continue; + + std::string teId = teTag->getString("id"); + + TilePos globalPos( + teTag->getInt32("x"), + teTag->getInt32("y"), + teTag->getInt32("z") + ); + + TileEntity* te = TileEntity::createId(teId, globalPos); + if (te) + { + te->load(*teTag); + addTileEntity(te); + } + } +} +TileEntity* LevelChunk::getTileEntity(const ChunkTilePos& pos) +{ + TilePos globalPos(m_chunkPos, pos.y); + globalPos.x += pos.x; + globalPos.z += pos.z; + + std::map::iterator it = m_tileEntities.find(globalPos); + if (it != m_tileEntities.end()) + return it->second; + return nullptr; +} + +void LevelChunk::saveTileEntities(CompoundTag& chunkTag) +{ + if (m_tileEntities.empty()) return; + + ListTag* list = new ListTag(); + for (std::map::iterator it = m_tileEntities.begin(); it != m_tileEntities.end(); ++it) + { + CompoundTag* teTag = new CompoundTag(); + it->second->save(*teTag); + list->add(teTag); + } + chunkTag.put("TileEntities", list); +} \ No newline at end of file diff --git a/source/world/level/levelgen/chunk/LevelChunk.hpp b/source/world/level/levelgen/chunk/LevelChunk.hpp index f25faf097..f648f7287 100644 --- a/source/world/level/levelgen/chunk/LevelChunk.hpp +++ b/source/world/level/levelgen/chunk/LevelChunk.hpp @@ -17,6 +17,7 @@ #include "world/level/levelgen/chunk/ChunkPos.hpp" #include "world/level/levelgen/chunk/ChunkTilePos.hpp" #include "world/level/levelgen/chunk/DataLayer.hpp" +#include "world/level/tileentity/TileEntity.h" class Level; class AABB; @@ -26,6 +27,7 @@ class LevelChunk { private: void _init(); + std::map m_tileEntities; protected: LevelChunk() { _init(); } public: @@ -72,6 +74,17 @@ class LevelChunk virtual Random getRandom(int32_t l); virtual void recalcHeight(const ChunkTilePos& pos); virtual bool isEmpty(); + void addTileEntity(TileEntity* te); + void removeTileEntity(const TilePos &pos); + TileEntity* getTileEntity(const ChunkTilePos& pos); + + // For the heartbeat + void tickTileEntities(); + + void loadTileEntities(const ListTag &list); + + void saveTileEntities(CompoundTag &chunkTag); + //... public: diff --git a/source/world/level/levelgen/chunk/RandomLevelSource.cpp b/source/world/level/levelgen/chunk/RandomLevelSource.cpp index 613fa88b8..2134c829b 100644 --- a/source/world/level/levelgen/chunk/RandomLevelSource.cpp +++ b/source/world/level/levelgen/chunk/RandomLevelSource.cpp @@ -89,7 +89,7 @@ LevelChunk* RandomLevelSource::getChunk(const ChunkPos& pos) // @PARITY: Java Edition Beta 1.6 uses the m_largeCaveFeature. #ifdef FEATURE_CAVES - m_largeCaveFeature.apply(this, m_pLevel, tilePos.x, tilePos.z, pLevelData, 0); + m_largeCaveFeature.apply(this, m_pLevel, pos.x, pos.z, pLevelData, 0); #endif return pChunk; @@ -597,7 +597,7 @@ void RandomLevelSource::postProcess(ChunkSource* src, const ChunkPos& pos) TilePos o(m_random.nextInt(16), m_random.nextInt(128), m_random.nextInt(16)); - VegetationFeature(Tile::tallGrass->id, data).place(m_pLevel, &m_random, TilePos(tp.x + 8 + o.x, o.y, tp.z + 8 + o.z)); + VegetationFeature(Tile::tallGrass->m_ID, data).place(m_pLevel, &m_random, TilePos(tp.x + 8 + o.x, o.y, tp.z + 8 + o.z)); } vegetationCount = 0; @@ -610,7 +610,7 @@ void RandomLevelSource::postProcess(ChunkSource* src, const ChunkPos& pos) int xo = m_random.nextInt(16); int yo = m_random.nextInt(128); int zo = m_random.nextInt(16); - VegetationFeature(Tile::deadBush->id, 0, 4).place(m_pLevel, &m_random, TilePos(tp.x + 8 + xo, yo, tp.z + 8 + zo)); + VegetationFeature(Tile::deadBush->m_ID, 0, 4).place(m_pLevel, &m_random, TilePos(tp.x + 8 + xo, yo, tp.z + 8 + zo)); } #endif float* tempBlock = m_pLevel->getBiomeSource()->getTemperatureBlock(tp.x + 8, tp.z + 8, 16, 16); diff --git a/source/world/level/storage/ExternalFileLevelStorage.cpp b/source/world/level/storage/ExternalFileLevelStorage.cpp index 18b1e297c..29f327638 100644 --- a/source/world/level/storage/ExternalFileLevelStorage.cpp +++ b/source/world/level/storage/ExternalFileLevelStorage.cpp @@ -12,7 +12,9 @@ #include "common/Logger.hpp" #include "nbt/CompoundTag.hpp" +#include "nbt/ListTag.hpp" #include "nbt/NbtIo.hpp" +#include "world/level/levelgen/chunk/LevelChunk.hpp" #include "network/RakIO.hpp" #include "world/entity/EntityFactory.hpp" #include "world/level/Level.hpp" @@ -250,7 +252,19 @@ LevelChunk* ExternalFileLevelStorage::load(Level* level, const ChunkPos& pos) } pBitStream->Read((char*)pChunk->m_updateMap, sizeof pChunk->m_updateMap); - + + // Load tile entities if present (saves from newer versions append them here) + if (pBitStream->GetWriteOffset() > pBitStream->GetReadOffset()) { + RakDataInput dis(*pBitStream); + CompoundTag* teTag = NbtIo::read(dis); + if (teTag) { + const ListTag* list = teTag->getList("TileEntities"); + if (list) pChunk->loadTileEntities(*list); + teTag->deleteChildren(); + delete teTag; + } + } + delete[] pBitStream->GetData(); delete pBitStream; @@ -374,6 +388,15 @@ void ExternalFileLevelStorage::save(Level* level, LevelChunk* chunk) bs.Write((const char*)chunk->m_updateMap, sizeof chunk->m_updateMap); + // Save tile entities (furnaces, etc.) appended after the block data + CompoundTag teTag; + chunk->saveTileEntities(teTag); + if (!teTag.isEmpty()) { + RakDataOutput dos(bs); + NbtIo::write(teTag, dos); + teTag.deleteChildren(); + } + m_pRegionFile->writeChunk(chunk->m_chunkPos, bs); } diff --git a/source/world/level/tileentity/TileEntity.cpp b/source/world/level/tileentity/TileEntity.cpp new file mode 100644 index 000000000..ef01f28a9 --- /dev/null +++ b/source/world/level/tileentity/TileEntity.cpp @@ -0,0 +1,21 @@ +// +// Created by dooli on 2026/03/23. +// + +#include "TileEntity.h" + +#include "world/inventory/FurnaceTileEntity.h" +#include "world/level/Level.hpp" + +TileEntity* TileEntity::createId(const std::string& id, const TilePos& pos) { + if (id == "Furnace") return new FurnaceTileEntity(pos); + // Easy to add more later! (e.g. ChestTileEntity when implemented) + return NULL; +} + +void TileEntity::markDirty() { + if (level) { + LevelChunk* chunk = level->getChunkAt(pos); + if (chunk) chunk->markUnsaved(); + } +} diff --git a/source/world/level/tileentity/TileEntity.h b/source/world/level/tileentity/TileEntity.h new file mode 100644 index 000000000..cbcf3ea53 --- /dev/null +++ b/source/world/level/tileentity/TileEntity.h @@ -0,0 +1,46 @@ +#ifndef TILEENTITY_H +#define TILEENTITY_H + +#include +#include "world/level/TilePos.hpp" +#include "nbt/CompoundTag.hpp" + +class Level; + +class TileEntity { +public: + // 1. Declaration order: pos, then level, then id + TilePos pos; + Level* level; + std::string id; + + // 2. Constructor: Must match the order above! + TileEntity(const std::string& id, const TilePos& pos) + : pos(pos), level(NULL), id(id) {} + + // 3. VIRTUAL destructor is required for OOP! + virtual ~TileEntity() {} + + virtual void load(const CompoundTag& tag) { + // If 'getInt' fails, check CompoundTag.hpp for 'getInt32' or 'put' + pos.x = tag.getInt32("x"); + pos.y = tag.getInt32("y"); + pos.z = tag.getInt32("z"); + } + + virtual void save(CompoundTag& tag) const { + tag.putString("id", id); + tag.putInt32("x", pos.x); + tag.putInt32("y", pos.y); + tag.putInt32("z", pos.z); + } + + virtual void tick() {} + + // Marks the owning chunk as needing to be saved. + void markDirty(); + + static TileEntity* createId(const std::string& id, const TilePos& pos); +}; + +#endif \ No newline at end of file diff --git a/source/world/tile/CraftingTableTile.cpp b/source/world/tile/CraftingTableTile.cpp index f6cd821ab..b9e1b293a 100644 --- a/source/world/tile/CraftingTableTile.cpp +++ b/source/world/tile/CraftingTableTile.cpp @@ -21,7 +21,6 @@ bool CraftingTableTile::use(Level* level, const TilePos& pos, Player* player) return true; } } - int CraftingTableTile::getTexture(Facing::Name face) const { switch (face) { @@ -31,3 +30,4 @@ int CraftingTableTile::getTexture(Facing::Name face) const default: return m_TextureFrame; } } + diff --git a/source/world/tile/FurnaceTile.cpp b/source/world/tile/FurnaceTile.cpp new file mode 100644 index 000000000..029de2d27 --- /dev/null +++ b/source/world/tile/FurnaceTile.cpp @@ -0,0 +1,106 @@ +// +// Created by Dominic Hann on 23/3/2026. +// + +#include "FurnaceTile.hpp" +#include "world/entity/Player.hpp" +#include "world/level/Level.hpp" +#include "common/Mth.hpp" + +bool FurnaceTile::s_swappingLitState = false; + +FurnaceTile::FurnaceTile(TileID id) : Tile(id, TEXTURE_FURNACE_FRONT, Material::stone) +{ +} + +bool FurnaceTile::use(Level* level, const TilePos& pos, Player* player) +{ + if (player->isSneaking() && !player->getSelectedItem().isEmpty()) + { + return false; + } + if (level->m_bIsClientSide) + { + return true; + } + else + { + player->openFurnace(pos); + return true; + } +} + +int FurnaceTile::getTexture(const Facing::Name face) const +{ + bool lit = (m_ID == TILE_FURNACE_LIT); + switch (face) { + case Facing::UP: return TEXTURE_FURNACE_TOP; + case Facing::DOWN: return Tile::rock->getTexture(face); + case Facing::SOUTH: return lit ? TEXTURE_FURNACE_LIT : TEXTURE_FURNACE_FRONT; + default: return TEXTURE_FURNACE_SIDE; + } +} + +int FurnaceTile::getTexture(Facing::Name face, TileData data) const +{ + bool lit = (m_ID == TILE_FURNACE_LIT); + int facing = data & 3; + bool isFront = (face == Facing::SOUTH && facing == 0) || + (face == Facing::WEST && facing == 1) || + (face == Facing::NORTH && facing == 2) || + (face == Facing::EAST && facing == 3); + switch (face) { + case Facing::UP: return TEXTURE_FURNACE_TOP; + case Facing::DOWN: return Tile::rock->getTexture(face); + default: + if (isFront) return lit ? TEXTURE_FURNACE_LIT : TEXTURE_FURNACE_FRONT; + return TEXTURE_FURNACE_SIDE; + } +} + +void FurnaceTile::setPlacedBy(Level* level, const TilePos& pos, Mob* mob) +{ + int rot = Mth::floor(0.5f + (mob->m_rot.x * 4.0f / 360.0f)) & 3; + TileData data = 0; + switch (rot) { + case 0: data = 2; break; + case 1: data = 3; break; + case 2: data = 0; break; + case 3: data = 1; break; + } + level->setData(pos, data); +} + +void FurnaceTile::onPlace(Level* level, const TilePos& pos) { + Tile::onPlace(level, pos); + if (!s_swappingLitState) + level->setTileEntity(pos.x, pos.y, pos.z, new FurnaceTileEntity(pos)); +} + +void FurnaceTile::onRemove(Level* level, const TilePos& pos) { + if (!s_swappingLitState) { + FurnaceTileEntity* furnace = (FurnaceTileEntity*)level->getTileEntity(pos.x, pos.y, pos.z); + if (furnace) { + for (int i = 0; i < furnace->getContainerSize(); i++) { + ItemStack& item = furnace->getItem(i); + if (!item.isEmpty()) { + Vec3 spawnPos = Vec3(pos.x + 0.5f, pos.y + 0.5f, pos.z + 0.5f); + ItemEntity* entity = new ItemEntity(level, spawnPos, item); + entity->m_throwTime = 10; + level->addEntity(entity); + } + } + } + level->removeTileEntity(pos.x, pos.y, pos.z); + } + Tile::onRemove(level, pos); +} + +int FurnaceTile::getResource(TileData data, Random* pRandom) const +{ + return TILE_FURNACE; +} + +TileEntity* FurnaceTile::createTileEntity(const TilePos& pos) { + return new FurnaceTileEntity(pos); +} \ No newline at end of file diff --git a/source/world/tile/FurnaceTile.hpp b/source/world/tile/FurnaceTile.hpp new file mode 100644 index 000000000..cefe5868c --- /dev/null +++ b/source/world/tile/FurnaceTile.hpp @@ -0,0 +1,32 @@ +// +// Created by Dominic Hann on 23/3/2026. +// + +#ifndef NBCRAFT_FURNACETILE_HPP +#define NBCRAFT_FURNACETILE_HPP +#include "Tile.hpp" + + +class FurnaceTile : public Tile { +public: + FurnaceTile(TileID id); + +public: + bool use(Level*, const TilePos& pos, Player*) override; + int getResource(TileData, Random*) const override; + int getTexture(Facing::Name face) const override; + int getTexture(Facing::Name face, TileData data) const override; + virtual void onPlace(Level* level, const TilePos& pos) override; + virtual void onRemove(Level* level, const TilePos& pos) override; + virtual void setPlacedBy(Level* level, const TilePos& pos, Mob* mob) override; + + virtual TileEntity* createTileEntity(const TilePos& pos); + + // Set to true before swapping between TILE_FURNACE and TILE_FURNACE_LIT + // to prevent onRemove/onPlace from deleting/creating the tile entity. + static bool s_swappingLitState; +}; + + + +#endif //NBCRAFT_FURNACETILE_HPP \ No newline at end of file diff --git a/source/world/tile/Tile.cpp b/source/world/tile/Tile.cpp index 861449de6..a3b1c1b55 100644 --- a/source/world/tile/Tile.cpp +++ b/source/world/tile/Tile.cpp @@ -55,7 +55,7 @@ #include "RocketLauncherTile.hpp" //#include "RedStoneDustTile.hpp" #include "CraftingTableTile.hpp" -//#include "FurnaceTile.hpp" +#include "FurnaceTile.hpp" #include "TallGrass.hpp" #include "DeadBush.hpp" //#include "Fern.hpp" @@ -780,6 +780,19 @@ void Tile::initTiles() ->setSoundType(Tile::SOUND_WOOD) ->setDescriptionId("workbench"); + Tile::furnace = (new FurnaceTile(TILE_FURNACE)) + ->init() + ->setDestroyTime(2.5f) + ->setSoundType(Tile::SOUND_STONE) + ->setDescriptionId("smelting"); + + Tile::furnaceLit = (new FurnaceTile(TILE_FURNACE_LIT)) + ->init() + ->setDestroyTime(2.5f) + ->setSoundType(Tile::SOUND_STONE) + ->setDescriptionId("smelting") + ->setLightEmission(0.875f); + Tile::crops = (new CropsTile(TILE_WHEAT, TEXTURE_WHEAT_0)) ->init() ->setDestroyTime(0.0f) @@ -1279,4 +1292,6 @@ Tile *Tile::web, *Tile::fence, *Tile::craftingTable, + *Tile::furnace, + *Tile::furnaceLit, *Tile::crops; diff --git a/source/world/tile/Tile.hpp b/source/world/tile/Tile.hpp index f9695c6fc..51a1092a8 100644 --- a/source/world/tile/Tile.hpp +++ b/source/world/tile/Tile.hpp @@ -23,6 +23,7 @@ #include "world/level/TileEvent.hpp" #include "world/phys/Vec3.hpp" #include "world/phys/HitResult.hpp" +#include "world/level/tileentity/TileEntity.h" class Level; class Entity; @@ -84,6 +85,7 @@ class Tile virtual void neighborChanged(Level*, const TilePos& pos, TileID tile); virtual void onPlace(Level*, const TilePos& pos); virtual void onRemove(Level*, const TilePos& pos); + virtual TileEntity* createTileEntity(const TilePos& pos) { return nullptr; } virtual int getResource(TileData, Random*) const; virtual int getResourceCount(Random*) const; virtual float getDestroyProgress(Player*) const; @@ -241,6 +243,8 @@ class Tile * web, * fence, * craftingTable, + * furnace, + * furnaceLit, * crops; public: @@ -291,3 +295,5 @@ class FullTile TileID getTypeId() const { return _tileType->m_ID; } Tile* getType() const { return _tileType; } }; + +