#pragma once #include "StarRect.hpp" #include "StarSectorArray2D.hpp" namespace Star { // Storage container for world tiles that understands the sector based // non-euclidean nature of the World. // // All RectI regions in this class are assumed to be right/top exclusive, so // each tile covered by a RectI region must be strictly contained within the // region to be included. template class TileSectorArray { public: typedef TileT Tile; static unsigned const SectorSize = SectorSizeT; typedef SectorArray2D SectorArray; typedef typename SectorArray::Sector Sector; typedef typename SectorArray::Array Array; typedef typename SectorArray::ArrayPtr ArrayPtr; TileSectorArray(); TileSectorArray(Vec2U const& size, Tile defaultTile = Tile()); void init(Vec2U const& size, Tile defaultTile = Tile()); Vec2U size() const; Tile defaultTile() const; // Returns true if this sector is within the size bounds, regardless of // loaded / unloaded status. bool sectorValid(Sector const& sector) const; Sector sectorFor(Vec2I const& pos) const; // Return all valid sectors within a given range, regardless of loaded / // unloaded status. List validSectorsFor(RectI const& region) const; // Returns the region for this sector, which is SectorSize x SectorSize // large. RectI sectorRegion(Sector const& sector) const; // Returns adjacent sectors in any given integral movement, in sectors. Sector adjacentSector(Sector const& sector, Vec2I const& sectorMovement); // Load a sector into the active sector array. void loadSector(Sector const& sector, ArrayPtr array); // Load with a sector full of the default tile. void loadDefaultSector(Sector const& sector); // Make a copy of a sector ArrayPtr copySector(Sector const& sector); // Take a sector out of the sector array. ArrayPtr unloadSector(Sector const& sector); bool sectorLoaded(Sector sector) const; List loadedSectors() const; size_t loadedSectorCount() const; // Will return null if the sector is unloaded. Array const* sectorArray(Sector sector) const; Array* sectorArray(Sector sector); bool tileLoaded(Vec2I const& pos) const; Tile const& tile(Vec2I const& pos) const; // Will return nullptr if the position is invalid. Tile* modifyTile(Vec2I const& pos); // Function signature here is (Vec2I const&, Tile const&). Will be called // for the entire region, valid or not. If tile positions are not valid, // they will be called with the defaultTile. template void tileEach(RectI const& region, Function&& function) const; // Behaves like tileEach, but gathers the results of calling the function into // a MultiArray template MultiArray, 2> tileEachResult(RectI const& region, Function&& function) const; // Fastest way to copy data from the tile array to a given target array. // Takes a multi-array and a region and a function, resizes the multi-array // to be the size of the given region, and then calls the given function on // each tile in the region with this signature: // function(MultiArray::Element& target, Vec2I const& position, Tile const& source); // Called with the defaultTile for out of range positions. template void tileEachTo(MultiArray& results, RectI const& region, Function&& function) const; // Function signature here is (Vec2I const&, Tile&). If a tile position // within this range is not valid, the function *will not* be called for that // position. template void tileEval(RectI const& region, Function&& function); // Will not be called for parts of the region that are not valid positions. template void tileEachColumns(RectI const& region, Function&& function) const; template void tileEvalColumns(RectI const& region, Function&& function); // Searches for a tile that satisfies a given condition in a block-area. // Returns true on the first instance found. Passed in function must accept // (Vec2I const&, Tile const&). template bool tileSatisfies(RectI const& region, Function&& function) const; // Same, but uses a radius of 'distance', which is inclusive on all sides. // In other words, calling tileSatisfies({0, 0}, 1, ) should be // equivalent to calling tileSatisfies({-1, -1}, {3, 3}, ). template bool tileSatisfies(Vec2I const& pos, unsigned distance, Function&& function) const; private: struct SplitRect { RectI rect; int xOffset; }; // function must return bool to continue iteration template bool tileEachAbortable(RectI const& region, Function&& function) const; // Splits rects along the world wrap line and wraps the x coordinate for each // rect into world space. Also returns the integral x offset to transform // back into the input rect range. StaticList splitRect(RectI rect) const; // Clamp the rect to entirely within valid tile spaces in y dimension RectI yClampRect(RectI const& r) const; Vec2U m_worldSize; Tile m_default; SectorArray m_tileSectors; }; template unsigned const TileSectorArray::SectorSize; template TileSectorArray::TileSectorArray() {} template TileSectorArray::TileSectorArray(Vec2U const& size, Tile defaultTile) { init(size, std::move(defaultTile)); } template void TileSectorArray::init(Vec2U const& size, Tile defaultTile) { m_worldSize = size; // Initialize to enough sectors to fit world size at least. m_tileSectors.init((size[0] + SectorSize - 1) / SectorSize, (size[1] + SectorSize - 1) / SectorSize); m_default = std::move(defaultTile); } template Vec2U TileSectorArray::size() const { return m_worldSize; } template Tile TileSectorArray::defaultTile() const { return m_default; } template auto TileSectorArray::sectorFor(Vec2I const& pos) const -> Sector { return m_tileSectors.sectorFor((unsigned)pmod(pos[0], m_worldSize[0]), (unsigned)pos[1]); } template bool TileSectorArray::sectorValid(Sector const& sector) const { return m_tileSectors.sectorValid(sector); } template auto TileSectorArray::validSectorsFor(RectI const& region) const -> List { List sectors; for (auto const& split : splitRect(yClampRect(region))) { auto sectorRange = m_tileSectors.sectorRange(split.rect.xMin(), split.rect.yMin(), split.rect.width(), split.rect.height()); sectors.reserve(sectors.size() + (sectorRange.max[0] - sectorRange.min[0]) * (sectorRange.max[1] - sectorRange.min[1])); for (size_t x = sectorRange.min[0]; x < sectorRange.max[0]; ++x) { for (size_t y = sectorRange.min[1]; y < sectorRange.max[1]; ++y) sectors.append({x, y}); } } return sectors; } template RectI TileSectorArray::sectorRegion(Sector const& sector) const { Vec2I sectorCorner(m_tileSectors.sectorCorner(sector)); return RectI::withSize(sectorCorner, {min(SectorSize, m_worldSize[0] - sectorCorner[0]), min(SectorSize, m_worldSize[1] - sectorCorner[1])}); } template auto TileSectorArray::adjacentSector(Sector const& sector, Vec2I const& sectorMovement) -> Sector { // This works because the only smaller than SectorSize sectors are on the // world wrap point, and there is only one vertical line of them, but it's // very not-obvious that it works. Vec2I corner(m_tileSectors.sectorCorner(sector)); corner += sectorMovement * SectorSize; return sectorFor(corner); } template void TileSectorArray::loadSector(Sector const& sector, ArrayPtr tile) { if (sectorValid(sector)) m_tileSectors.loadSector(sector, std::move(tile)); } template void TileSectorArray::loadDefaultSector(Sector const& sector) { if (sectorValid(sector)) m_tileSectors.loadSector(sector, std::make_unique(m_default)); } template auto TileSectorArray::copySector(Sector const& sector) -> ArrayPtr { if (sectorValid(sector)) return m_tileSectors.copySector(sector); else return {}; } template auto TileSectorArray::unloadSector(Sector const& sector) -> ArrayPtr { if (sectorValid(sector)) return m_tileSectors.takeSector(sector); else return {}; } template bool TileSectorArray::sectorLoaded(Sector sector) const { if (sectorValid(sector)) return m_tileSectors.sectorLoaded(sector); else return false; } template auto TileSectorArray::loadedSectors() const -> List { return m_tileSectors.loadedSectors(); } template size_t TileSectorArray::loadedSectorCount() const { return m_tileSectors.loadedSectorCount(); } template auto TileSectorArray::sectorArray(Sector sector) const -> Array const * { if (sectorValid(sector)) return m_tileSectors.sector(sector); else return nullptr; } template auto TileSectorArray::sectorArray(Sector sector) -> Array * { if (sectorValid(sector)) return m_tileSectors.sector(sector); else return nullptr; } template bool TileSectorArray::tileLoaded(Vec2I const& pos) const { if (pos[1] < 0 || pos[1] >= (int)m_worldSize[1]) return false; unsigned xind = (unsigned)pmod(pos[0], m_worldSize[0]); unsigned yind = (unsigned)pos[1]; return m_tileSectors.get(xind, yind) != nullptr; } template Tile const& TileSectorArray::tile(Vec2I const& pos) const { if (pos[1] < 0 || pos[1] >= (int)m_worldSize[1]) return m_default; unsigned xind = (unsigned)pmod(pos[0], m_worldSize[0]); unsigned yind = (unsigned)pos[1]; Tile const* tile = m_tileSectors.get(xind, yind); if (tile) return *tile; else return m_default; } template Tile* TileSectorArray::modifyTile(Vec2I const& pos) { if (pos[1] < 0 || pos[1] >= (int)m_worldSize[1]) return nullptr; unsigned xind = (unsigned)pmod(pos[0], m_worldSize[0]); unsigned yind = (unsigned)pos[1]; return m_tileSectors.get(xind, yind); } template template void TileSectorArray::tileEach(RectI const& region, Function&& function) const { tileEachAbortable(region, [&](Vec2I const& pos, Tile const& tile) { function(pos, tile); return true; }); } template template MultiArray, 2> TileSectorArray::tileEachResult(RectI const& region, Function&& function) const { MultiArray, 2> res; tileEachTo(res, region, [&](auto& res, Vec2I const& pos, Tile const& tile) { res = function(pos, tile); }); return res; } template template void TileSectorArray::tileEachTo(MultiArray& results, RectI const& region, Function&& function) const { if (region.isEmpty()) { results.setSize({0, 0}); return; } int xArrayOffset = -region.xMin(); int yArrayOffset = -region.yMin(); results.setSize(Vec2S(region.size())); for (auto const& split : splitRect(region)) { auto clampedRect = yClampRect(split.rect); if (!clampedRect.isEmpty()) { m_tileSectors.evalColumns(clampedRect.xMin(), clampedRect.yMin(), clampedRect.width(), clampedRect.height(), [&](size_t x, size_t y, Tile const* column, size_t columnSize) { size_t arrayColumnIndex = (x + split.xOffset + xArrayOffset) * results.size(1) + y + yArrayOffset; if (column) { for (size_t i = 0; i < columnSize; ++i) function(results.atIndex(arrayColumnIndex + i), Vec2I((int)x + split.xOffset, y + i), column[i]); } else { for (size_t i = 0; i < columnSize; ++i) function(results.atIndex(arrayColumnIndex + i), Vec2I((int)x + split.xOffset, y + i), m_default); } return true; }, true); } // Call with default tile for tiles outside of the y-range (to ensure that // every index in the rect gets called) for (int x = split.rect.xMin(); x < split.rect.xMax(); ++x) { for (int y = split.rect.yMin(); y < min(split.rect.yMax(), 0); ++y) function(results(x + split.xOffset + xArrayOffset, y + yArrayOffset), Vec2I(x + split.xOffset, y), m_default); } for (int x = split.rect.xMin(); x < split.rect.xMax(); ++x) { for (int y = max(split.rect.yMin(), m_worldSize[1]); y < split.rect.yMax(); ++y) function(results(x + split.xOffset + xArrayOffset, y + yArrayOffset), Vec2I(x + split.xOffset, y), m_default); } } } template template void TileSectorArray::tileEval(RectI const& region, Function&& function) { for (auto const& split : splitRect(region)) { auto clampedRect = yClampRect(split.rect); if (!clampedRect.isEmpty()) { // If non-const variant, do not call function if tile not loaded (pass // false to evalEmpty in sector array) auto fwrapper = [&](unsigned x, unsigned y, Tile* tile) { function(Vec2I((int)x + split.xOffset, (int)y), *tile); return true; }; m_tileSectors.eval(clampedRect.xMin(), clampedRect.yMin(), clampedRect.width(), clampedRect.height(), fwrapper, false); } } } template template void TileSectorArray::tileEachColumns(RectI const& region, Function&& function) const { const_cast(this)->tileEvalColumns( region, [&](Vec2I const& pos, Tile* tiles, size_t size) { function(pos, (Tile const*)tiles, size); }); } template template void TileSectorArray::tileEvalColumns(RectI const& region, Function&& function) { for (auto const& split : splitRect(region)) { auto clampedRect = yClampRect(split.rect); if (!clampedRect.isEmpty()) { auto fwrapper = [&](size_t x, size_t y, Tile* column, size_t columnSize) { function(Vec2I((int)x + split.xOffset, (int)y), column, columnSize); return true; }; m_tileSectors.evalColumns(clampedRect.xMin(), clampedRect.yMin(), clampedRect.width(), clampedRect.height(), fwrapper, false); } } } template template bool TileSectorArray::tileSatisfies(Vec2I const& pos, unsigned distance, Function&& function) const { return tileSatisfies(RectI::withSize(pos - Vec2I::filled(distance), Vec2I::filled(distance * 2 + 1)), function); } template template bool TileSectorArray::tileSatisfies(RectI const& region, Function&& function) const { return !tileEachAbortable(region, [&](Vec2I const& pos, Tile const& tile) { return !function(pos, tile); }); } template template bool TileSectorArray::tileEachAbortable(RectI const& region, Function&& function) const { for (auto const& split : splitRect(region)) { auto clampedRect = yClampRect(split.rect); if (!clampedRect.isEmpty()) { // If const variant, call function with default tile if not loaded. auto fwrapper = [&](unsigned x, unsigned y, Tile const* tile) { if (!tile) tile = &m_default; return function(Vec2I((int)x + split.xOffset, y), *tile); }; if (!m_tileSectors.eval(clampedRect.xMin(), clampedRect.yMin(), clampedRect.width(), clampedRect.height(), fwrapper, true)) return false; } // Call with default tile for tiles outside of the y-range (to ensure // that every index in the rect gets called) for (int x = split.rect.xMin(); x < split.rect.xMax(); ++x) { for (int y = split.rect.yMin(); y < min(split.rect.yMax(), 0); ++y) { if (!function(Vec2I(x + split.xOffset, y), m_default)) return false; } } for (int x = split.rect.xMin(); x < split.rect.xMax(); ++x) { for (int y = max(split.rect.yMin(), m_worldSize[1]); y < split.rect.yMax(); ++y) if (!function(Vec2I(x + split.xOffset, y), m_default)) return false; } } return true; } template auto TileSectorArray::splitRect(RectI rect) const -> StaticList { // TODO: Offset here does not support rects outside of -m_worldSize[0] to 2 * m_worldSize[0]! starAssert(rect.xMin() >= -(int)m_worldSize[0] && rect.xMax() <= 2 * (int)m_worldSize[0]); // any rect at least the width of the world is equivalent to a rect that spans the width of the world exactly if (rect.width() >= (int)m_worldSize[0]) return{SplitRect{RectI(0, rect.yMin(), m_worldSize[0], rect.yMax()), 0}}; if (rect.isEmpty()) return {}; int width = rect.width(); int xMin = pmod(rect.xMin(), m_worldSize[0]); int xOffset = rect.xMin() - xMin; rect.setXMin(xMin); rect.setXMax(xMin + width); if (rect.xMin() < (int)m_worldSize[0] && rect.xMax() > (int)m_worldSize[0]) { return { SplitRect{RectI(rect.xMin(), rect.yMin(), m_worldSize[0], rect.yMax()), xOffset}, SplitRect{RectI(0, rect.yMin(), rect.xMax() - m_worldSize[0], rect.yMax()), xOffset + (int)m_worldSize[0]} }; } else { return {SplitRect{rect, xOffset}}; } } template RectI TileSectorArray::yClampRect(RectI const& r) const { return RectI(r.xMin(), clamp(r.yMin(), 0, m_worldSize[1]), r.xMax(), clamp(r.yMax(), 0, m_worldSize[1])); } }