diff --git a/src/map.cpp b/src/map.cpp
index 315b89cba9..696d024be7 100644
--- a/src/map.cpp
+++ b/src/map.cpp
@@ -249,6 +249,7 @@ bool CircularTileSearch(TileIndex *tile, uint size, TestTileOnSearchProc proc, v
/* If the length of the side is uneven, the center has to be checked
* separately, as the pattern of uneven sides requires to go around the center */
if (proc(*tile, user_data)) return true;
+ if (size < 2) return false;
/* If tile test is not successful, get one tile up,
* ready for a test in first circle around center tile */
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
index 94579147fa..b346cd98c9 100644
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -15,5 +15,6 @@ add_test_files(
test_network_crypto.cpp
test_script_admin.cpp
test_window_desc.cpp
+ tilearea.cpp
utf8.cpp
)
diff --git a/src/tests/tilearea.cpp b/src/tests/tilearea.cpp
new file mode 100644
index 0000000000..85c7b50acb
--- /dev/null
+++ b/src/tests/tilearea.cpp
@@ -0,0 +1,132 @@
+/*
+ * This file is part of OpenTTD.
+ * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
+ * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see .
+ */
+
+/** @file tilearea.cpp Test functionality from tilearea_type. */
+
+#include "../stdafx.h"
+
+#include "../3rdparty/catch2/catch.hpp"
+
+#include "../tilearea_type.h"
+#include "../map_func.h"
+
+#include "../safeguards.h"
+
+struct TileCoord {
+ uint x, y;
+};
+
+static void TestSpiralTileSequence(TileCoord center, uint diameter, std::span expected)
+{
+ auto tile = TileXY(center.x, center.y);
+
+ std::vector result;
+ for (auto ti : SpiralTileSequence(tile, diameter)) {
+ result.push_back(ti);
+ }
+ REQUIRE(result.size() == expected.size());
+ for (size_t i = 0; i < result.size(); ++i) {
+ CHECK(TileX(result[i]) == expected[i].x);
+ CHECK(TileY(result[i]) == expected[i].y);
+ }
+
+ result.clear();
+ CircularTileSearch(&tile, diameter,
+ +[](TileIndex tile, void *user_data) -> bool {
+ reinterpret_cast *>(user_data)->push_back(tile);
+ return false;
+ }, &result);
+ REQUIRE(result.size() == expected.size());
+ for (size_t i = 0; i < result.size(); ++i) {
+ CHECK(TileX(result[i]) == expected[i].x);
+ CHECK(TileY(result[i]) == expected[i].y);
+ }
+}
+
+static void TestSpiralTileSequence(TileCoord start_north, uint radius, uint w, uint h, std::span expected)
+{
+ auto tile = TileXY(start_north.x, start_north.y);
+
+ std::vector result;
+ for (auto ti : SpiralTileSequence(tile, radius, w, h)) {
+ result.push_back(ti);
+ }
+ REQUIRE(result.size() == expected.size());
+ for (size_t i = 0; i < result.size(); ++i) {
+ CHECK(TileX(result[i]) == expected[i].x);
+ CHECK(TileY(result[i]) == expected[i].y);
+ }
+
+ result.clear();
+ CircularTileSearch(&tile, radius, w, h,
+ +[](TileIndex tile, void *user_data) -> bool {
+ reinterpret_cast *>(user_data)->push_back(tile);
+ return false;
+ }, &result);
+ REQUIRE(result.size() == expected.size());
+ for (size_t i = 0; i < result.size(); ++i) {
+ CHECK(TileX(result[i]) == expected[i].x);
+ CHECK(TileY(result[i]) == expected[i].y);
+ }
+}
+
+TEST_CASE("SpiralTileSequence - minimum")
+{
+ Map::Allocate(64, 64);
+
+ TileCoord expected[] = {{63, 63}};
+ TestSpiralTileSequence({63, 63}, 1, expected);
+ TestSpiralTileSequence({63, 63}, 2, expected);
+ TestSpiralTileSequence({63, 63}, 1, 0, 0, expected);
+ TestSpiralTileSequence({63, 63}, 1, 2, 2, expected);
+}
+
+TEST_CASE("SpiralTileSequence - odd")
+{
+ Map::Allocate(64, 64);
+
+ TileCoord expected[] = {
+ {1, 1},
+ {2, 0}, {1, 0}, {0, 0}, {0, 1}, {0, 2}, {1, 2}, {2, 2}, {2, 1},
+ {0, 3}, {1, 3}, {2, 3}, {3, 3}, {3, 2}, {3, 1}, {3, 0},
+ };
+ TestSpiralTileSequence({1, 1}, 5, expected);
+}
+
+TEST_CASE("SpiralTileSequence - even")
+{
+ Map::Allocate(64, 64);
+
+ TileCoord expected[] = {
+ {2, 1}, {1, 1}, {1, 2}, {2, 2},
+ {3, 0}, {2, 0}, {1, 0}, {0, 0}, {0, 1}, {0, 2}, {0, 3}, {1, 3}, {2, 3}, {3, 3}, {3, 2}, {3, 1},
+ {0, 4}, {1, 4}, {2, 4}, {3, 4}, {4, 4}, {4, 3}, {4, 2}, {4, 1}, {4, 0},
+ };
+ TestSpiralTileSequence({1, 1}, 6, expected);
+ TestSpiralTileSequence({1, 1}, 3, 0, 0, expected);
+}
+
+TEST_CASE("SpiralTileSequence - zero hole")
+{
+ Map::Allocate(64, 64);
+
+ TileCoord expected[] = {
+ {5, 2}, {4, 2}, {3, 2}, {2, 2}, {2, 3}, {3, 3}, {4, 3}, {5, 3},
+ {6, 1}, {5, 1}, {4, 1}, {3, 1}, {2, 1}, {1, 1}, {1, 2}, {1, 3}, {1, 4}, {2, 4}, {3, 4}, {4, 4}, {5, 4}, {6, 4}, {6, 3}, {6, 2},
+ };
+ TestSpiralTileSequence({2, 2}, 2, 2, 0, expected);
+}
+
+TEST_CASE("SpiralTileSequence - normal hole")
+{
+ Map::Allocate(64, 64);
+
+ TileCoord expected[] = {
+ {4, 2}, {3, 2}, {2, 2}, {2, 3}, {2, 4}, {2, 5}, {3, 5}, {4, 5}, {4, 4}, {4, 3},
+ };
+ TestSpiralTileSequence({2, 2}, 1, 1, 2, expected);
+}
diff --git a/src/tilearea.cpp b/src/tilearea.cpp
index 83705a82c1..09560c9f5c 100644
--- a/src/tilearea.cpp
+++ b/src/tilearea.cpp
@@ -295,3 +295,105 @@ TileIterator &DiagonalTileIterator::operator++()
}
return std::make_unique(corner1, corner2);
}
+
+/**
+ * See SpiralTileSequence constructor for description.
+ */
+SpiralTileIterator::SpiralTileIterator(TileIndex center, uint diameter) :
+ max_radius(diameter / 2),
+ cur_radius(0),
+ dir(DIAGDIR_BEGIN)
+{
+ assert(diameter > 0);
+
+ if (diameter % 2 == 1) {
+ this->extent.fill(1);
+ this->dir = INVALID_DIAGDIR; // special case for odd diameters, see Increment()
+ this->position = 0;
+
+ this->x = TileX(center);
+ this->y = TileY(center);
+ } else {
+ this->extent.fill(0);
+ this->dir = DIAGDIR_BEGIN;
+ this->InitPosition();
+
+ /* Start with the west corner of the center 2x2 rect */
+ this->x = TileX(center) + 1;
+ this->y = TileY(center);
+ }
+ this->SkipOutsideMap();
+}
+
+/**
+ * See SpiralTileSequence constructor for description.
+ */
+SpiralTileIterator::SpiralTileIterator(TileIndex start_north, uint radius, uint w, uint h) :
+ max_radius(radius),
+ extent{w, h, w, h},
+ cur_radius(0),
+ dir(DIAGDIR_BEGIN),
+ /* first tile is the west corner */
+ x(TileX(start_north) + w + 1),
+ y(TileY(start_north))
+{
+ assert(max_radius > 0);
+ this->InitPosition();
+ this->SkipOutsideMap();
+}
+
+/**
+ * Advance the internal state until it reaches a valid tile or the end.
+ */
+void SpiralTileIterator::SkipOutsideMap()
+{
+ while (!this->IsEnd() && (this->x >= Map::SizeX() || this->y >= Map::SizeY())) this->Increment();
+}
+
+/**
+ * Initialise "position" after "dir" was changed.
+ */
+void SpiralTileIterator::InitPosition()
+{
+ this->position = this->extent[this->dir] + this->cur_radius * 2 + 1;
+}
+
+/**
+ * Advance the internal state to the next potential tile.
+ * The tile may be outside the map though.
+ */
+void SpiralTileIterator::Increment()
+{
+ assert(!this->IsEnd());
+
+ /* Special value for first tile in areas with odd diameter */
+ if (this->dir == INVALID_DIAGDIR) {
+ const auto west = TileIndexDiffCByDir(DIR_W);
+ this->x += west.x;
+ this->y += west.y;
+ this->dir = DIAGDIR_BEGIN;
+ this->InitPosition();
+ return;
+ }
+
+ /* Step to the next 'neighbour' in the circular line */
+ const auto diff = TileIndexDiffCByDiagDir(this->dir);
+ this->x += diff.x;
+ this->y += diff.y;
+ --this->position;
+ if (this->position > 0) return;
+
+ /* Corner reached, switch direction */
+ ++this->dir;
+
+ if (this->dir == DIAGDIR_END) {
+ /* Jump to next circle */
+ const auto west = TileIndexDiffCByDir(DIR_W);
+ this->x += west.x;
+ this->y += west.y;
+ ++this->cur_radius;
+ this->dir = DIAGDIR_BEGIN;
+ }
+
+ this->InitPosition();
+}
diff --git a/src/tilearea_type.h b/src/tilearea_type.h
index 9add7cfe8b..85a910a97b 100644
--- a/src/tilearea_type.h
+++ b/src/tilearea_type.h
@@ -253,4 +253,122 @@ public:
}
};
+/**
+ * Helper class for SpiralTileSequence.
+ */
+class SpiralTileIterator {
+public:
+ using value_type = TileIndex;
+ using difference_type = std::ptrdiff_t;
+ using iterator_category = std::forward_iterator_tag;
+ using pointer = void;
+ using reference = void;
+
+ SpiralTileIterator(TileIndex center, uint diameter);
+ SpiralTileIterator(TileIndex start_north, uint radius, uint w, uint h);
+
+ bool operator==(const SpiralTileIterator &rhs) const { return this->x == rhs.x && this->y == rhs.y; }
+ bool operator==(const std::default_sentinel_t &) const { return this->IsEnd(); }
+
+ TileIndex operator*() const { return TileXY(this->x, this->y); }
+
+ SpiralTileIterator &operator++()
+ {
+ this->Increment();
+ this->SkipOutsideMap();
+ return *this;
+ }
+
+ SpiralTileIterator operator++(int)
+ {
+ SpiralTileIterator result = *this;
+ ++*this;
+ return result;
+ }
+
+private:
+ /* set by constructor, const afterwards */
+ uint max_radius;
+ std::array extent;
+
+ /* mutable iterator state */
+ uint cur_radius;
+ DiagDirection dir;
+ uint position;
+ uint x, y;
+
+ void SkipOutsideMap();
+ void InitPosition();
+ void Increment();
+
+ /**
+ * Test whether the iterator reached the end.
+ */
+ bool IsEnd() const
+ {
+ return this->cur_radius == this->max_radius && this->dir != INVALID_DIAGDIR;
+ }
+};
+
+/**
+ * Generate TileIndices around a center tile or tile area, with increasing distance.
+ */
+class SpiralTileSequence {
+public:
+ /**
+ * Generate TileIndices for a square area around a center tile.
+ *
+ * The size of the square is given by the length of the edge.
+ * If the size is even, the south extent will be larger than the north extent.
+ *
+ * Example for diameter=4, [ ] is the "center":
+ * 1
+ * 1 1
+ * 1 [0] 1
+ * 1 0 0 1
+ * 1 0 1
+ * 1 1
+ * 1
+ * The sequence starts with the "0" tiles, and continues with the shells around it.
+ *
+ * @param center Center of the square area.
+ * @param diameter Edge length of the square.
+ * @pre diameter > 0
+ * @note This constructor uses a "diameter", unlike the other constructor using a "radius".
+ */
+ SpiralTileSequence(TileIndex center, uint diameter) : start(center, diameter) {}
+
+ /**
+ * Generate TileIndices for a rectangular area with an optional rectangular hole in the center.
+ * The TileIndices will be sorted by increasing distance from the center (hole).
+ *
+ * Example for radius=2, w=2, h=1, [ ] is "start_north":
+ * 1
+ * 1 1
+ * 1 [0] 1
+ * 1 0 0 1
+ * 1 0 H 0 1
+ * 1 0 H 0 1
+ * 1 0 0 1
+ * 1 0 1
+ * 1 1
+ * 1
+ * The sequence starts with the "0" tiles, and continues with the shells around it.
+ *
+ * @param start_north Tile directly north from the center hole.
+ * @param radius Radial distance between outer rectangle and center hole.
+ * @param w Width of the inner rectangular hole.
+ * @param h Height of the inner rectangular hole.
+ * @pre radius > 0
+ * @note This constructor uses a "radius", unlike the other constructor using a "diameter".
+ */
+ SpiralTileSequence(TileIndex start_north, uint radius, uint w, uint h) : start(start_north, radius, w, h) {}
+
+ SpiralTileIterator begin() const { return start; }
+ std::default_sentinel_t end() const { return std::default_sentinel_t(); }
+
+private:
+ SpiralTileIterator start;
+};
+
#endif /* TILEAREA_TYPE_H */