diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3c09a5a534..c31102d260 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -56,6 +56,7 @@ set(tfs_SRC ${CMAKE_CURRENT_LIST_DIR}/protocollogin.cpp ${CMAKE_CURRENT_LIST_DIR}/protocolold.cpp ${CMAKE_CURRENT_LIST_DIR}/protocolstatus.cpp + ${CMAKE_CURRENT_LIST_DIR}/quadtree.cpp ${CMAKE_CURRENT_LIST_DIR}/rsa.cpp ${CMAKE_CURRENT_LIST_DIR}/scheduler.cpp ${CMAKE_CURRENT_LIST_DIR}/script.cpp @@ -143,6 +144,7 @@ set(tfs_HDR ${CMAKE_CURRENT_LIST_DIR}/protocolold.h ${CMAKE_CURRENT_LIST_DIR}/protocolstatus.h ${CMAKE_CURRENT_LIST_DIR}/pugicast.h + ${CMAKE_CURRENT_LIST_DIR}/quadtree.h ${CMAKE_CURRENT_LIST_DIR}/rsa.h ${CMAKE_CURRENT_LIST_DIR}/scheduler.h ${CMAKE_CURRENT_LIST_DIR}/script.h diff --git a/src/map.cpp b/src/map.cpp index 9475d35ab5..beab84d3cd 100644 --- a/src/map.cpp +++ b/src/map.cpp @@ -11,6 +11,7 @@ #include "iomap.h" #include "iomapserialize.h" #include "monster.h" +#include "quadtree.h" #include "spectators.h" extern Game g_game; @@ -67,17 +68,7 @@ Tile* Map::getTile(uint16_t x, uint16_t y, uint8_t z) const if (z >= MAP_MAX_LAYERS) { return nullptr; } - - const QTreeLeafNode* leaf = QTreeNode::getLeafStatic(&root, x, y); - if (!leaf) { - return nullptr; - } - - const Floor* floor = leaf->getFloor(z); - if (!floor) { - return nullptr; - } - return floor->tiles[x & FLOOR_MASK][y & FLOOR_MASK]; + return tfs::map::quadtree::find_tile(x, y, z); } void Map::setTile(uint16_t x, uint16_t y, uint8_t z, Tile* newTile) @@ -87,41 +78,7 @@ void Map::setTile(uint16_t x, uint16_t y, uint8_t z, Tile* newTile) return; } - QTreeLeafNode::newLeaf = false; - QTreeLeafNode* leaf = root.createLeaf(x, y, 15); - - if (QTreeLeafNode::newLeaf) { - // update north - QTreeLeafNode* northLeaf = root.getLeaf(x, y - FLOOR_SIZE); - if (northLeaf) { - northLeaf->leafS = leaf; - } - - // update west leaf - QTreeLeafNode* westLeaf = root.getLeaf(x - FLOOR_SIZE, y); - if (westLeaf) { - westLeaf->leafE = leaf; - } - - // update south - QTreeLeafNode* southLeaf = root.getLeaf(x, y + FLOOR_SIZE); - if (southLeaf) { - leaf->leafS = southLeaf; - } - - // update east - QTreeLeafNode* eastLeaf = root.getLeaf(x + FLOOR_SIZE, y); - if (eastLeaf) { - leaf->leafE = eastLeaf; - } - } - - Floor* floor = leaf->createFloor(z); - uint32_t offsetX = x & FLOOR_MASK; - uint32_t offsetY = y & FLOOR_MASK; - - Tile*& tile = floor->tiles[offsetX][offsetY]; - if (tile) { + if (auto tile = tfs::map::quadtree::find_tile(x, y, z)) { TileItemVector* items = newTile->getItemList(); if (items) { for (auto it = items->rbegin(), end = items->rend(); it != end; ++it) { @@ -137,7 +94,7 @@ void Map::setTile(uint16_t x, uint16_t y, uint8_t z, Tile* newTile) } delete newTile; } else { - tile = newTile; + tfs::map::quadtree::create_tile(x, y, z, newTile); } } @@ -147,39 +104,31 @@ void Map::removeTile(uint16_t x, uint16_t y, uint8_t z) return; } - const QTreeLeafNode* leaf = QTreeNode::getLeafStatic(&root, x, y); - if (!leaf) { - return; - } - - const Floor* floor = leaf->getFloor(z); - if (!floor) { + auto tile = tfs::map::quadtree::find_tile(x, y, z); + if (!tile) { return; } - Tile* tile = floor->tiles[x & FLOOR_MASK][y & FLOOR_MASK]; - if (tile) { - if (const CreatureVector* creatures = tile->getCreatures()) { - for (int32_t i = creatures->size(); --i >= 0;) { - if (Player* player = (*creatures)[i]->getPlayer()) { - g_game.internalTeleport(player, player->getTown()->getTemplePosition(), false, FLAG_NOLIMIT); - } else { - g_game.removeCreature((*creatures)[i]); - } + if (const CreatureVector* creatures = tile->getCreatures()) { + for (int32_t i = creatures->size(); --i >= 0;) { + if (Player* player = (*creatures)[i]->getPlayer()) { + g_game.internalTeleport(player, player->getTown()->getTemplePosition(), false, FLAG_NOLIMIT); + } else { + g_game.removeCreature((*creatures)[i]); } } + } - if (TileItemVector* items = tile->getItemList()) { - for (auto it = items->begin(), end = items->end(); it != end; ++it) { - g_game.internalRemoveItem(*it); - } + if (TileItemVector* items = tile->getItemList()) { + for (auto it = items->begin(), end = items->end(); it != end; ++it) { + g_game.internalRemoveItem(*it); } + } - Item* ground = tile->getGround(); - if (ground) { - g_game.internalRemoveItem(ground); - tile->setGround(nullptr); - } + Item* ground = tile->getGround(); + if (ground) { + g_game.internalRemoveItem(ground); + tile->setGround(nullptr); } } @@ -243,8 +192,8 @@ bool Map::placeCreature(const Position& centerPos, Creature* creature, bool exte Cylinder* toCylinder = tile->queryDestination(index, *creature, &toItem, flags); toCylinder->internalAddThing(creature); - const Position& dest = toCylinder->getPosition(); - getQTNode(dest.x, dest.y)->addCreature(creature); + const Position& destPos = toCylinder->getPosition(); + tfs::map::quadtree::push_creature(destPos.x, destPos.y, creature); return true; } @@ -281,14 +230,7 @@ void Map::moveCreature(Creature& creature, Tile& newTile, bool forceTeleport /* // remove the creature oldTile.removeThing(&creature, 0); - QTreeLeafNode* leaf = getQTNode(oldPos.x, oldPos.y); - QTreeLeafNode* new_leaf = getQTNode(newPos.x, newPos.y); - - // Switch the node ownership - if (leaf != new_leaf) { - leaf->removeCreature(&creature); - new_leaf->addCreature(&creature); - } + tfs::map::quadtree::move_creature(oldPos.x, oldPos.y, newPos.x, newPos.y, &creature); // add the creature newTile.addThing(&creature); @@ -329,66 +271,6 @@ void Map::moveCreature(Creature& creature, Tile& newTile, bool forceTeleport /* newTile.postAddNotification(&creature, &oldTile, 0); } -void Map::getSpectatorsInternal(SpectatorVec& spectators, const Position& centerPos, int32_t minRangeX, - int32_t maxRangeX, int32_t minRangeY, int32_t maxRangeY, int32_t minRangeZ, - int32_t maxRangeZ, bool onlyPlayers) const -{ - auto min_y = centerPos.y + minRangeY; - auto min_x = centerPos.x + minRangeX; - auto max_y = centerPos.y + maxRangeY; - auto max_x = centerPos.x + maxRangeX; - - int32_t minoffset = centerPos.getZ() - maxRangeZ; - uint16_t x1 = std::min(0xFFFF, std::max(0, (min_x + minoffset))); - uint16_t y1 = std::min(0xFFFF, std::max(0, (min_y + minoffset))); - - int32_t maxoffset = centerPos.getZ() - minRangeZ; - uint16_t x2 = std::min(0xFFFF, std::max(0, (max_x + maxoffset))); - uint16_t y2 = std::min(0xFFFF, std::max(0, (max_y + maxoffset))); - - int32_t startx1 = x1 - (x1 % FLOOR_SIZE); - int32_t starty1 = y1 - (y1 % FLOOR_SIZE); - int32_t endx2 = x2 - (x2 % FLOOR_SIZE); - int32_t endy2 = y2 - (y2 % FLOOR_SIZE); - - const QTreeLeafNode* startLeaf = - QTreeNode::getLeafStatic(&root, startx1, starty1); - const QTreeLeafNode* leafS = startLeaf; - const QTreeLeafNode* leafE; - - for (int_fast32_t ny = starty1; ny <= endy2; ny += FLOOR_SIZE) { - leafE = leafS; - for (int_fast32_t nx = startx1; nx <= endx2; nx += FLOOR_SIZE) { - if (leafE) { - const CreatureVector& node_list = (onlyPlayers ? leafE->player_list : leafE->creature_list); - for (Creature* creature : node_list) { - const Position& cpos = creature->getPosition(); - if (minRangeZ > cpos.z || maxRangeZ < cpos.z) { - continue; - } - - int16_t offsetZ = centerPos.getOffsetZ(cpos); - if ((min_y + offsetZ) > cpos.y || (max_y + offsetZ) < cpos.y || (min_x + offsetZ) > cpos.x || - (max_x + offsetZ) < cpos.x) { - continue; - } - - spectators.emplace_back(creature); - } - leafE = leafE->leafE; - } else { - leafE = QTreeNode::getLeafStatic(&root, nx + FLOOR_SIZE, ny); - } - } - - if (leafS) { - leafS = leafS->leafS; - } else { - leafS = QTreeNode::getLeafStatic(&root, startx1, ny + FLOOR_SIZE); - } - } -} - void Map::getSpectators(SpectatorVec& spectators, const Position& centerPos, bool multifloor /*= false*/, bool onlyPlayers /*= false*/, int32_t minRangeX /*= 0*/, int32_t maxRangeX /*= 0*/, int32_t minRangeY /*= 0*/, int32_t maxRangeY /*= 0*/) @@ -470,8 +352,37 @@ void Map::getSpectators(SpectatorVec& spectators, const Position& centerPos, boo maxRangeZ = centerPos.z; } - getSpectatorsInternal(spectators, centerPos, minRangeX, maxRangeX, minRangeY, maxRangeY, minRangeZ, maxRangeZ, - onlyPlayers); + auto min_y = centerPos.y + minRangeY; + auto min_x = centerPos.x + minRangeX; + auto max_y = centerPos.y + maxRangeY; + auto max_x = centerPos.x + maxRangeX; + + int32_t minoffset = centerPos.getZ() - maxRangeZ; + uint16_t x1 = std::min(0xFFFF, std::max(0, (min_x + minoffset))); + uint16_t y1 = std::min(0xFFFF, std::max(0, (min_y + minoffset))); + + int32_t maxoffset = centerPos.getZ() - minRangeZ; + uint16_t x2 = std::min(0xFFFF, std::max(0, (max_x + maxoffset))); + uint16_t y2 = std::min(0xFFFF, std::max(0, (max_y + maxoffset))); + + for (auto creature : tfs::map::quadtree::find_in_range(x1, y1, x2, y2)) { + if (onlyPlayers && !creature->getPlayer()) { + continue; + } + + const auto& position = creature->getPosition(); + if (minRangeZ > position.z || maxRangeZ < position.z) { + continue; + } + + auto offsetZ = centerPos.getOffsetZ(position); + if ((min_y + offsetZ) > position.y || (max_y + offsetZ) < position.y || (min_x + offsetZ) > position.x || + (max_x + offsetZ) < position.x) { + continue; + } + + spectators.emplace_back(creature); + } if (cacheResult) { if (onlyPlayers) { @@ -939,96 +850,6 @@ int_fast32_t AStarNodes::getTileWalkCost(const Creature& creature, const Tile* t return cost; } -// Floor -Floor::~Floor() -{ - for (auto& row : tiles) { - for (auto tile : row) { - delete tile; - } - } -} - -// QTreeNode -QTreeNode::~QTreeNode() -{ - for (auto* ptr : child) { - delete ptr; - } -} - -QTreeLeafNode* QTreeNode::getLeaf(uint32_t x, uint32_t y) -{ - if (leaf) { - return static_cast(this); - } - - QTreeNode* node = child[((x & 0x8000) >> 15) | ((y & 0x8000) >> 14)]; - if (!node) { - return nullptr; - } - return node->getLeaf(x << 1, y << 1); -} - -QTreeLeafNode* QTreeNode::createLeaf(uint32_t x, uint32_t y, uint32_t level) -{ - if (!isLeaf()) { - uint32_t index = ((x & 0x8000) >> 15) | ((y & 0x8000) >> 14); - if (!child[index]) { - if (level != FLOOR_BITS) { - child[index] = new QTreeNode(); - } else { - child[index] = new QTreeLeafNode(); - QTreeLeafNode::newLeaf = true; - } - } - return child[index]->createLeaf(x * 2, y * 2, level - 1); - } - return static_cast(this); -} - -// QTreeLeafNode -bool QTreeLeafNode::newLeaf = false; - -QTreeLeafNode::~QTreeLeafNode() -{ - for (auto* ptr : array) { - delete ptr; - } -} - -Floor* QTreeLeafNode::createFloor(uint32_t z) -{ - if (!array[z]) { - array[z] = new Floor(); - } - return array[z]; -} - -void QTreeLeafNode::addCreature(Creature* c) -{ - creature_list.push_back(c); - - if (c->getPlayer()) { - player_list.push_back(c); - } -} - -void QTreeLeafNode::removeCreature(Creature* c) -{ - auto iter = std::find(creature_list.begin(), creature_list.end(), c); - assert(iter != creature_list.end()); - *iter = creature_list.back(); - creature_list.pop_back(); - - if (c->getPlayer()) { - iter = std::find(player_list.begin(), player_list.end(), c); - assert(iter != player_list.end()); - *iter = player_list.back(); - player_list.pop_back(); - } -} - uint32_t Map::clean() const { uint64_t start = OTSYS_TIME(); diff --git a/src/map.h b/src/map.h index cd2f6e7147..3b4e25a7a3 100644 --- a/src/map.h +++ b/src/map.h @@ -12,7 +12,7 @@ class Creature; -static constexpr int32_t MAP_MAX_LAYERS = 16; +inline constexpr int32_t MAP_MAX_LAYERS = 16; struct FindPathParams; struct AStarNode @@ -52,96 +52,7 @@ class AStarNodes using SpectatorCache = std::map; -static constexpr int32_t FLOOR_BITS = 3; -static constexpr int32_t FLOOR_SIZE = (1 << FLOOR_BITS); -static constexpr int32_t FLOOR_MASK = (FLOOR_SIZE - 1); - -struct Floor -{ - constexpr Floor() = default; - ~Floor(); - - // non-copyable - Floor(const Floor&) = delete; - Floor& operator=(const Floor&) = delete; - - Tile* tiles[FLOOR_SIZE][FLOOR_SIZE] = {}; -}; - class FrozenPathingConditionCall; -class QTreeLeafNode; - -class QTreeNode -{ -public: - constexpr QTreeNode() = default; - virtual ~QTreeNode(); - - // non-copyable - QTreeNode(const QTreeNode&) = delete; - QTreeNode& operator=(const QTreeNode&) = delete; - - bool isLeaf() const { return leaf; } - - QTreeLeafNode* getLeaf(uint32_t x, uint32_t y); - - template - static Leaf getLeafStatic(Node node, uint32_t x, uint32_t y) - { - do { - node = node->child[((x & 0x8000) >> 15) | ((y & 0x8000) >> 14)]; - if (!node) { - return nullptr; - } - - x <<= 1; - y <<= 1; - } while (!node->leaf); - return static_cast(node); - } - - QTreeLeafNode* createLeaf(uint32_t x, uint32_t y, uint32_t level); - -protected: - bool leaf = false; - -private: - QTreeNode* child[4] = {}; - - friend class Map; -}; - -class QTreeLeafNode final : public QTreeNode -{ -public: - QTreeLeafNode() - { - leaf = true; - newLeaf = true; - } - ~QTreeLeafNode(); - - // non-copyable - QTreeLeafNode(const QTreeLeafNode&) = delete; - QTreeLeafNode& operator=(const QTreeLeafNode&) = delete; - - Floor* createFloor(uint32_t z); - Floor* getFloor(uint8_t z) const { return array[z]; } - - void addCreature(Creature* c); - void removeCreature(Creature* c); - -private: - static bool newLeaf; - QTreeLeafNode* leafS = nullptr; - QTreeLeafNode* leafE = nullptr; - Floor* array[MAP_MAX_LAYERS] = {}; - CreatureVector creature_list; - CreatureVector player_list; - - friend class Map; - friend class QTreeNode; -}; /** * Map class. @@ -247,11 +158,6 @@ class Map std::map waypoints; - QTreeLeafNode* getQTNode(uint16_t x, uint16_t y) - { - return QTreeNode::getLeafStatic(&root, x, y); - } - Spawns spawns; Towns towns; Houses houses; @@ -260,19 +166,12 @@ class Map SpectatorCache spectatorCache; SpectatorCache playersSpectatorCache; - QTreeNode root; - std::filesystem::path spawnfile; std::filesystem::path housefile; uint32_t width = 0; uint32_t height = 0; - // Actually scans the map for spectators - void getSpectatorsInternal(SpectatorVec& spectators, const Position& centerPos, int32_t minRangeX, - int32_t maxRangeX, int32_t minRangeY, int32_t maxRangeY, int32_t minRangeZ, - int32_t maxRangeZ, bool onlyPlayers) const; - friend class Game; friend class IOMap; }; diff --git a/src/quadtree.cpp b/src/quadtree.cpp new file mode 100644 index 0000000000..ce74668b8c --- /dev/null +++ b/src/quadtree.cpp @@ -0,0 +1,213 @@ +#include "quadtree.h" + +namespace { +std::array nodes = {}; + +uint8_t create_index(uint16_t x, uint16_t y) { return ((x & 0x8000) >> 15) | ((y & 0x8000) >> 14); } + +QuadTree* find_leaf(QuadTree* node, uint16_t x, uint16_t y) +{ + if (node->is_leaf()) { + return node; + } + + auto index = create_index(x, y); + if (auto node_child = node->get_child(index)) { + return find_leaf(node_child, x << 1, y << 1); + } + return nullptr; +} + +Leaf* find_leaf_in_root(uint16_t x, uint16_t y) +{ + auto index = create_index(x, y); + if (auto node = nodes[index]) { + if (auto leaf = find_leaf(node, x, y)) { + return static_cast(leaf); + } + } + return nullptr; +} + +/** + * @brief Establishes relationships with neighboring Leaf nodes. + * + * This section checks for neighboring leaf + * nodes (north, south, east, and west) + * and establishes two-way relationships if those neighboring leaves are found. + * + * @param {x} The x-coordinate of the leaf node in the quadtree. + * @param {y} The + * y-coordinate of the leaf node in the quadtree. + * + * The following relationships are updated: + * - The north + * neighbor's south_leaf pointer is updated to point to this leaf. + * - The west neighbor's east_leaf pointer is + * updated to point to this leaf. + * - This leaf's south_leaf pointer is updated to point to the south neighbor, if + * found. + * - This leaf's east_leaf pointer is updated to point to the east neighbor, if found. + */ +void update_leaf_neighbors(uint16_t x, uint16_t y) +{ + if (auto leaf = find_leaf_in_root(x, y)) { + // update north + if (auto north_leaf = find_leaf_in_root(x, y - TILE_GRID_SIZE)) { + north_leaf->south_leaf = leaf; + } + + // update west + if (auto west_leaf = find_leaf_in_root(x - TILE_GRID_SIZE, y)) { + west_leaf->east_leaf = leaf; + } + + // update south + if (auto south_leaf = find_leaf_in_root(x, y + TILE_GRID_SIZE)) { + leaf->south_leaf = south_leaf; + } + + // update east + if (auto east_leaf = find_leaf_in_root(x + TILE_GRID_SIZE, y)) { + leaf->east_leaf = east_leaf; + } + } + + void create_leaf_node(uint16_t x, uint16_t y, uint8_t z, QuadTree * node) + { + if (node->is_leaf()) { + return; + } + + auto index = create_index(x, y); + auto node_child = node->get_child(index); + if (!node_child) { + if (z == TILE_GRID_BITS) { + node_child = new Leaf(); + } else { + node_child = new Node(); + } + + node->set_child(index, node_child); + } + + create_leaf_node(x * 2, y * 2, z - 1, node_child); + } + + void create_leaf_in_root(uint16_t x, uint16_t y) + { + auto index = create_index(x, y); + if (!nodes[index]) { + nodes[index] = new Node(); + } + + create_leaf_node(x, y, (MAP_MAX_LAYERS - 1), nodes[index]); + update_leaf_neighbors(x, y); + } + +} // namespace + +std::experimental::generator tfs::map::quadtree::find_in_range(uint16_t start_x, uint16_t start_y, + uint16_t end_x, uint16_t end_y) +{ + int32_t start_x_aligned = start_x - (start_x % TILE_GRID_SIZE); + int32_t start_y_aligned = start_y - (start_y % TILE_GRID_SIZE); + int32_t end_x_aligned = end_x - (end_x % TILE_GRID_SIZE); + int32_t end_y_aligned = end_y - (end_y % TILE_GRID_SIZE); + + if (auto start_leaf = find_leaf_in_root(start_x_aligned, start_y_aligned)) { + auto south_leaf = start_leaf; + + for (int32_t ny = start_y_aligned; ny <= end_y_aligned; ny += TILE_GRID_SIZE) { + auto east_leaf = south_leaf; + + for (int32_t nx = start_x_aligned; nx <= end_x_aligned; nx += TILE_GRID_SIZE) { + if (east_leaf) { + for (auto creature : east_leaf->creatures) { + co_yield creature; + } + + east_leaf = east_leaf->east_leaf; + } else { + east_leaf = find_leaf_in_root(nx + TILE_GRID_SIZE, ny); + } + } + + if (south_leaf) { + south_leaf = south_leaf->south_leaf; + } else { + south_leaf = find_leaf_in_root(start_x_aligned, ny + TILE_GRID_SIZE); + } + } + } +} + +Tile* tfs::map::quadtree::find_tile(uint16_t x, uint16_t y, uint8_t z) +{ + if (auto leaf = find_leaf_in_root(x, y)) { + // Find the tile at layer z, using TILE_INDEX_MASK to ensure that the x and y coordinates + // are within the bounds of the leaf (only the least significant bits are used). + return leaf->tiles[z][x & TILE_INDEX_MASK][y & TILE_INDEX_MASK]; + } + return nullptr; +} + +void tfs::map::quadtree::create_tile(uint16_t x, uint16_t y, uint8_t z, Tile* tile) +{ + create_leaf_in_root(x, y); + + if (auto leaf = find_leaf_in_root(x, y)) { + // Store the tile in the correct position in the tile array. + // Here, we also use TILE_INDEX_MASK to index correctly, ensuring that only + // the relevant bits of the x and y coordinates are used. + leaf->tiles[z][x & TILE_INDEX_MASK][y & TILE_INDEX_MASK] = tile; + } +} + +void tfs::map::quadtree::move_creature(uint16_t old_x, uint16_t old_y, uint16_t x, uint16_t y, Creature* creature) +{ + if (auto old_leaf = find_leaf_in_root(old_x, old_y)) { + if (auto leaf = find_leaf_in_root(x, y)) { + if (old_leaf != leaf) { + old_leaf->remove_creature(creature); + leaf->push_creature(creature); + } + } + } +} + +void tfs::map::quadtree::push_creature(uint16_t x, uint16_t y, Creature* creature) +{ + if (auto leaf = find_leaf_in_root(x, y)) { + leaf->push_creature(creature); + } +} + +void tfs::map::quadtree::remove_creature(uint16_t x, uint16_t y, Creature* creature) +{ + if (auto leaf = find_leaf_in_root(x, y)) { + leaf->remove_creature(creature); + } +} + +Node::~Node() +{ + for (auto node_ptr : nodes) { + delete node_ptr; + } +} + +Leaf::~Leaf() +{ + for (auto& layer : tiles) { + for (auto& row : layer) { + for (auto tile : row) { + delete tile; + } + } + } +} + +void Leaf::push_creature(Creature* creature) { creatures.insert(creature); } + +void Leaf::remove_creature(Creature* creature) { creatures.erase(creature); } diff --git a/src/quadtree.h b/src/quadtree.h new file mode 100644 index 0000000000..a3bada473d --- /dev/null +++ b/src/quadtree.h @@ -0,0 +1,247 @@ +// Copyright 2023 The Forgotten Server Authors. All rights reserved. +// Use of this source code is governed by the GPL-2.0 License that can be found in the LICENSE file. + +#ifndef FS_QUADTREE_H +#define FS_QUADTREE_H + +#include "otpch.h" + +#include "map.h" + +#include // TODO: Use std::generator in C++23. + +class Creature; +class Tile; + +namespace tfs::map::quadtree { + +/// @brief Finds creatures within the specified range. +std::experimental::generator find_in_range(uint16_t start_x, uint16_t start_y, uint16_t end_x, + uint16_t end_y); + +/// @brief Finds the tile at the specified coordinates and layer. +Tile* find_tile(uint16_t x, uint16_t y, uint8_t z); + +/// @brief Creates a tile at the specified coordinates and layer. +void create_tile(uint16_t x, uint16_t y, uint8_t z, Tile* tile); + +/// @brief Moves a creature from one location to another within the quadtree. +void move_creature(uint16_t old_x, uint16_t old_y, uint16_t x, uint16_t y, Creature* creature); + +/// @brief Adds a creature into the quadtree at the specified coordinates. +void push_creature(uint16_t x, uint16_t y, Creature* creature); + +/// @brief Removes a creature from the quadtree at the specified coordinates. +void remove_creature(uint16_t x, uint16_t y, Creature* creature); + +} // namespace tfs::map::quadtree + +/** + * @brief Base class representing a QuadTree. + * + * This class defines the interface for a QuadTree node. + * The class is non-copyable to prevent accidental copying of nodes. + */ +class QuadTree +{ +public: + /// @brief Default constructor for QuadTree. + constexpr QuadTree() = default; + + /** + * @brief Virtual destructor for QuadTree. + * + * The virtual destructor allows for proper cleanup + * of derived classes + * that may be allocated dynamically. + */ + virtual ~QuadTree() = default; + + /// Deleted copy constructor to ensure QuadTree is non-copyable. + QuadTree(const QuadTree&) = delete; + /// Deleted assignment operator to ensure QuadTree is non-copyable. + QuadTree& operator=(const QuadTree&) = delete; + + /** + * @brief Check if the node is a leaf node. + * @return true if the node is a leaf, false otherwise. + */ + virtual bool is_leaf() const = 0; + + /** + * @brief Set a child node at a specified index. + * + * This method assigns a child node to the + * current node at the given + * index. The index must be within valid bounds for child nodes (0 to 3). + * @param {index} The index at which to set the child node. + * @param {node} A pointer to the child node to be + * set. + */ + virtual void set_child(uint8_t index, QuadTree* node) = 0; + + /** + * @brief Get a child node at a specified index. + * @param {index} The index of the child node to + * retrieve (0 to 3). + * @return A pointer to the child node, or nullptr if no child exists. + */ + virtual QuadTree* get_child(uint8_t index) const = 0; +}; + +/// The number of bits used to represent the dimensions of a tile grid. +inline constexpr int32_t TILE_GRID_BITS = 3; +/// The size of the tile grid, calculated as 2 ^ TILE_GRID_BITS. +/// This value indicates the number of tiles that can fit along one dimension of the grid. +inline constexpr int32_t TILE_GRID_SIZE = (1 << TILE_GRID_BITS); +/// A mask to isolate the tile index within the bounds of the grid size. +/// This mask is used to ensure tile indices remain valid and wrap around correctly. +inline constexpr int32_t TILE_INDEX_MASK = (TILE_GRID_SIZE - 1); + +/** + * @class Node + * @brief Represents a node in a QuadTree. + * + * This class extends the QuadTree interface and + * implements a specific type of node that can have up to four child nodes. + */ +class Node final : public QuadTree +{ +public: + /// @brief Default constructor for QuadTree. + constexpr Node() = default; + ~Node(); + + /// Deleted copy constructor to ensure Node is non-copyable. + Node(const Node&) = delete; + /// Deleted assignment operator to ensure Node is non-copyable. + Node& operator=(const Node&) = delete; + + /** + * @brief Check if the node is a leaf node. + * + * This implementation always returns false, as Node + * objects are designed to have children. + * @return false, indicating that the node is not a leaf. + */ + bool is_leaf() const override { return false; } + + /** + * @brief Set a child node at a specified index. + * + * This method assigns a child node to the + * current node at the given index. The index must be in the range of 0 to 3. + * + * @param {index} The index + * at which to set the child node (0 to 3). + * @param {node} A pointer to the child QuadTree node to be set. + */ + void set_child(uint8_t index, QuadTree* node) override { nodes[index] = node; } + + /** + * @brief Get a child node at a specified index. + * + * This method retrieves the child node at the + * specified index. + * + * @param {index} The index of the child node to retrieve (0 to 3). + * @return A + * pointer to the child QuadTree node, or nullptr if no child exists. + */ + QuadTree* get_child(uint8_t index) const override { return nodes[index]; }; + +private: + /// An array storing pointers to the child nodes of the QuadTree node. + std::array nodes = {}; +}; + +/** + * @class Leaf + * @brief Represents a leaf node in a QuadTree. + * + * This class extends the QuadTree interface and + * is designed to hold data specific to a leaf node, such as creatures and tile information. + */ +class Leaf final : public QuadTree +{ +public: + /** + * @brief Constructor for Leaf. + * + * This constructor initializes the leaf node with the specified + * coordinates. + */ + constexpr Leaf() = default; + ~Leaf(); + + /// Deleted copy constructor to ensure Leaf is non-copyable. + Leaf(const Leaf&) = delete; + /// Deleted assignment operator to ensure Leaf is non-copyable. + Leaf& operator=(const Leaf&) = delete; + + /** + * @brief Check if the node is a leaf node. + * + * This implementation always returns true, + * indicating that this object is a leaf. + * @return true, indicating that the node is a Leaf. + */ + bool is_leaf() const override { return true; } + + /** + * @brief Set a child node at a specified index. + * + * This method does nothing, as leaf nodes + * cannot have children. + * @param {index} The index at which to set the child node (not used). + * @param + * {node} A pointer to the child node to be set (not used). + */ + void set_child(uint8_t, QuadTree*) override {} + + /** + * @brief Get a child node at a specified index. + * + * This method always returns nullptr, as leaf + * nodes do not have children. + * @param {index} The index of the child node to retrieve (not used). + * @return Always returns nullptr. + */ + QuadTree* get_child(uint8_t) const override { return nullptr; }; + + /** + * @brief Add a creature to the leaf. + * + * This method adds the specified creature to the Leaf + * node. + * @param {creature} A pointer to the creature to be added. + */ + void push_creature(Creature* creature); + + /** + * @brief Remove a creature from the leaf. + * + * This method removes the specified creature from + * the leaf node. + * @param {creature} A pointer to the creature to be removed. + */ + void remove_creature(Creature* creature); + + /** + * @brief A 3D array of pointers to tiles for the leaf across multiple layers. + * + * This array holds pointers to `Tile` objects, organized in a three-dimensional + * structure. The first dimension represents different layers, while the second + * and third dimensions represent the x and y coordinates of the tiles within + * each layer. Each layer can contain a grid of tiles. + */ + std::array, TILE_GRID_SIZE>, MAP_MAX_LAYERS> tiles = {}; + + /// @brief A set of creatures (monsters, NPCs and players) present in this leaf node. + std::set creatures; + + Leaf* south_leaf = nullptr; + Leaf* east_leaf = nullptr; +}; + +#endif // FS_QUADTREE_H diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 2e6d1b1a6d..d86ed77cad 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -2,6 +2,7 @@ set(tests_SRC ${CMAKE_CURRENT_LIST_DIR}/test_base64.cpp ${CMAKE_CURRENT_LIST_DIR}/test_generate_token.cpp ${CMAKE_CURRENT_LIST_DIR}/test_matrixarea.cpp + ${CMAKE_CURRENT_LIST_DIR}/test_quadtree.cpp ${CMAKE_CURRENT_LIST_DIR}/test_rsa.cpp ${CMAKE_CURRENT_LIST_DIR}/test_sha1.cpp ${CMAKE_CURRENT_LIST_DIR}/test_xtea.cpp diff --git a/src/tests/test_quadtree.cpp b/src/tests/test_quadtree.cpp new file mode 100644 index 0000000000..fad8e74236 --- /dev/null +++ b/src/tests/test_quadtree.cpp @@ -0,0 +1,53 @@ +#define BOOST_TEST_MODULE quadtree + +#include "../otpch.h" + +#include "../quadtree.h" + +#include + +BOOST_AUTO_TEST_CASE(test_find_in_range_single_creature) +{ + auto npc1 = new Npc(); + tfs::map::quadtree::push_creature(12, 12, npc1); + + int count = 0; + for (auto creature : tfs::map::quadtree::find_in_range(10, 10, 20, 20)) { + BOOST_TEST(creature == npc1); + count++; + } + + BOOST_TEST(count == 1); +} + +BOOST_AUTO_TEST_CASE(test_find_in_range_multiple_creature) +{ + auto npc1 = new Npc(); + auto npc2 = new Npc(); + tfs::map::quadtree::push_creature(10, 10, npc1); + tfs::map::quadtree::push_creature(15, 15, npc2); + + int count = 0; + for (auto creature : tfs::map::quadtree::find_in_range(5, 5, 20, 20)) { + BOOST_TEST(creature == creature1 || creature == creature2); + count++; + } + + BOOST_TEST(count == 2); +} + +BOOST_AUTO_TEST_CASE(test_find_in_range_no_creatures) +{ + auto npc1 = new Npc(); + auto npc2 = new Npc(); + tfs::map::quadtree::push_creature(50, 50, npc1); + tfs::map::quadtree::push_creature(60, 60, npc2); + + int count = 0; + for (auto creature : tfs::map::quadtree::find_in_range(5, 5, 20, 20)) { + BOOST_TEST(creature != npc1 && creature != npc2); + count++; + } + + BOOST_TEST(count == 0); +} diff --git a/src/tile.cpp b/src/tile.cpp index d731b4b051..1fbe3cc2dc 100644 --- a/src/tile.cpp +++ b/src/tile.cpp @@ -13,6 +13,7 @@ #include "mailbox.h" #include "monster.h" #include "movement.h" +#include "quadtree.h" #include "spectators.h" #include "teleport.h" #include "trashholder.h" @@ -1152,7 +1153,7 @@ bool Tile::hasCreature(Creature* creature) const void Tile::removeCreature(Creature* creature) { - g_game.map.getQTNode(tilePos.x, tilePos.y)->removeCreature(creature); + tfs::map::quadtree::remove_creature(tilePos.x, tilePos.y, creature); removeThing(creature, 0); } diff --git a/vc17/theforgottenserver.vcxproj b/vc17/theforgottenserver.vcxproj index fc8f930069..da43cf5867 100644 --- a/vc17/theforgottenserver.vcxproj +++ b/vc17/theforgottenserver.vcxproj @@ -112,6 +112,7 @@ AdvancedVectorExtensions $(VcpkgRoot)include\luajit;%(AdditionalIncludeDirectories) stdcpp20 + NotUsing true @@ -232,6 +233,7 @@ + @@ -323,6 +325,7 @@ +