diff --git a/source/core/inc/tactile/core/tile/tileset.hpp b/source/core/inc/tactile/core/tile/tileset.hpp new file mode 100644 index 0000000000..f97a51c9b2 --- /dev/null +++ b/source/core/inc/tactile/core/tile/tileset.hpp @@ -0,0 +1,256 @@ +// Copyright (C) 2024 Albin Johansson (GNU General Public License v3.0) + +#pragma once + +#include "tactile/base/container/expected.hpp" +#include "tactile/base/container/hash_map.hpp" +#include "tactile/base/container/maybe.hpp" +#include "tactile/base/container/vector.hpp" +#include "tactile/base/id.hpp" +#include "tactile/base/prelude.hpp" +#include "tactile/core/entity/entity.hpp" +#include "tactile/core/numeric/vec.hpp" +#include "tactile/core/util/matrix_extent.hpp" +#include "tactile/core/util/uuid.hpp" + +namespace tactile { + +struct CTexture; +struct TilesetSpec; +class Registry; + +/// \addtogroup Tileset +/// \{ + +/** + * Represents an sequence of tile identifiers. + */ +struct TileRange final +{ + /** The first associated tile identifier. */ + TileID first_id; + + /** The number of tile identifiers (starting at \c first_id). */ + int32 count; +}; + +/** + * Context component used to map tile identifiers to tilesets. + */ +struct CTileCache final +{ + /** Maps tile identifiers to the associated tilesets. */ + HashMap tileset_mapping; +}; + +/** + * A component that represents a tileset definition. + */ +struct CTileset final +{ + /** The logical size of all tiles. */ + Int2 tile_size; + + /** The size of the tileset. */ + MatrixExtent extent; + + /** Tracks the associated tiles. Indexed using \c TileIndex values. */ + Vector tiles; +}; + +/** + * A component that provides information about tilesets attached to maps. + * + * \note + * This component shouldn't be used by tilesets in tileset documents. + */ +struct CTilesetInstance final +{ + /** The associated global tile identifiers. */ + TileRange tile_range; + + /** Indicates whether the tileset is embedded in the map file when saved. */ + bool is_embedded; +}; + +/** + * Indicates whether an entity is a tileset. + * + * \details + * Tileset entities feature at least the following components. \n + * - \c CMeta \n + * - \c CTileset \n + * - \c CTexture \n + * - \c CViewport + * + * \param registry The associated registry. + * \param entity The entity to check. + * + * \return + * True if the entity is a tileset; false otherwise. + */ +[[nodiscard]] +auto is_tileset(const Registry& registry, EntityID entity) -> bool; + +/** + * Creates a tileset. + * + * \param registry The associated registry. + * \param spec The tileset specification. + * + * \return + * A tileset entity if successful; an invalid entity otherwise. + */ +[[nodiscard]] +auto make_tileset(Registry& registry, const TilesetSpec& spec) -> EntityID; + +/** + * Initializes a tileset "instance". + * + * \details + * Tileset instances feature the same components as normal tilesets, with the + * addition of map related information, such as the assigned tile range + * (see \c CTilesetInstance). This function should be used to initialize + * tilesets stored in map document registries. + * + * \details + * Tile identifiers are assigned sequentially in the tileset instance. + * For example, given a tileset with 100 tiles, a tileset instance created + * with the first tile identifier configured as 10 will claim the identifiers + * in the interval [10, 110). + * + * \details + * All tiles associated with the tileset will be registered in the \c CTileCache + * context component of the provided registry. + * + * \note + * The designated tile range must be unique to the tileset. + * + * \pre The registry must feature a \c CTileCache context component. + * \pre The specified entity must be a tileset. + * + * \param registry The associated registry. + * \param tileset_entity The target tileset. + * \param first_tile_id The first tile identifier to assign to the tileset. + * + * \return + * Nothing if successful; an error code otherwise. + */ +[[nodiscard]] +auto init_tileset_instance(Registry& registry, + EntityID tileset_entity, + TileID first_tile_id) -> Result; + +/** + * Destroys a tileset and all of its associated tiles. + * + * \details + * If the specified tileset features a \c CTilesetInstance component, then all + * associated tiles will be unregistered from the \c CTileCache context + * component in the provided registry. + * + * \pre The specified entity must be a tileset. + * \pre The registry must feature a \c CTileCache context component if the + * entity is a tileset instance. + * + * \param registry The associated registry. + * \param tileset_entity The tileset to destroy. + */ +void destroy_tileset(Registry& registry, EntityID tileset_entity); + +/** + * Creates a deep copy of a tileset. + * + * \pre The specified entity must be a tileset. + * + * \param registry The associated registry. + * \param tileset_entity The tileset that will be copied. + * + * \return + * A tileset entity. + */ +[[nodiscard]] +auto copy_tileset(Registry& registry, EntityID tileset_entity) -> EntityID; + +/** + * Returns the appearance of a tile in a tileset. + * + * \details + * This function should be used to determine how to render tiles correctly. + * For non-animated tiles, this function simply returns the given tile index. + * + * \complexity O(1). + * + * \pre The specified entity must be a tileset. + * + * \param registry The associated registry. + * \param tileset_entity The tileset that contains the tile. + * \param tile_index The index of the tile to query. + * + * \return + * The index of the tile that should be shown instead of the specified tile. + */ +[[nodiscard]] +auto get_tile_appearance(const Registry& registry, + EntityID tileset_entity, + TileIndex tile_index) -> TileIndex; + +/** + * Returns the tileset entity that features a given tile. + * + * \complexity O(1). + * + * \pre The registry must feature a \c CTileCache context component. + * + * \param registry The associated registry. + * \param tile_id The tile identifier to look for. + * + * \return + * A tileset entity if a tileset was found; an invalid entity otherwise. + */ +[[nodiscard]] +auto find_tileset(const Registry& registry, TileID tile_id) -> EntityID; + +/** + * Returns the local tile index of the tile associated with a given identifier. + * + * \pre The registry must feature a \c CTileCache context component. + * + * \param registry The associated registry. + * \param tile_id The tile identifier to convert. + * + * \return + * A tile index if successful; nothing otherwise. + */ +[[nodiscard]] +auto get_tile_index(const Registry& registry, + TileID tile_id) -> Maybe; + +/** + * Indicates whether the tiles in a tile range are available for use. + * + * \param registry The associated registry. + * \param range The tile range to check. + * + * \return + * True if the tile range is available; false otherwise. + */ +[[nodiscard]] +auto is_tile_range_available(const Registry& registry, + const TileRange& range) -> bool; + +/** + * Indicates whether a tile range contains a given tile identifier. + * + * \param tile_range The tile range. + * \param tile_id The tile identifier to look for. + * + * \return + * True if the tile is within the tile range; false otherwise. + */ +[[nodiscard]] +auto has_tile(const TileRange& tile_range, TileID tile_id) -> bool; + +/// \} + +} // namespace tactile diff --git a/source/core/inc/tactile/core/tile/tileset_spec.hpp b/source/core/inc/tactile/core/tile/tileset_spec.hpp new file mode 100644 index 0000000000..0e74f3a07f --- /dev/null +++ b/source/core/inc/tactile/core/tile/tileset_spec.hpp @@ -0,0 +1,28 @@ +// Copyright (C) 2024 Albin Johansson (GNU General Public License v3.0) + +#pragma once + +#include "tactile/base/prelude.hpp" +#include "tactile/core/io/texture.hpp" +#include "tactile/core/numeric/vec.hpp" + +namespace tactile { + +/// \addtogroup Tileset +/// \{ + +/** + * Represents the information needed to construct a tileset. + */ +struct TilesetSpec final +{ + /** The logical tile size. */ + Int2 tile_size; + + /** The associated texture. */ + CTexture texture; +}; + +/// \} + +} // namespace tactile diff --git a/source/core/src/tactile/core/tile/tileset.cpp b/source/core/src/tactile/core/tile/tileset.cpp new file mode 100644 index 0000000000..32f9bda27f --- /dev/null +++ b/source/core/src/tactile/core/tile/tileset.cpp @@ -0,0 +1,259 @@ +// Copyright (C) 2024 Albin Johansson (GNU General Public License v3.0) + +#include "tactile/core/tile/tileset.hpp" + +#include + +#include "tactile/core/debug/assert.hpp" +#include "tactile/core/debug/generic_error.hpp" +#include "tactile/core/entity/registry.hpp" +#include "tactile/core/io/texture.hpp" +#include "tactile/core/log/logger.hpp" +#include "tactile/core/log/set_log_scope.hpp" +#include "tactile/core/meta/meta.hpp" +#include "tactile/core/numeric/narrow.hpp" +#include "tactile/core/tile/animation.hpp" +#include "tactile/core/tile/tile.hpp" +#include "tactile/core/tile/tileset_spec.hpp" +#include "tactile/core/ui/viewport.hpp" +#include "tactile/core/util/lookup.hpp" + +namespace tactile { +namespace { + +void _create_tiles(Registry& registry, CTileset& tileset) +{ + const auto tile_count = tileset.extent.rows * tileset.extent.cols; + tileset.tiles.reserve(tile_count); + + for (usize index = 0; index < tile_count; ++index) { + const auto tile_index = narrow_cast(index); + const auto tile_entity = make_tile(registry, tile_index); + + tileset.tiles.push_back(tile_entity); + } +} + +} // namespace + +auto is_tileset(const Registry& registry, const EntityID entity) -> bool +{ + return registry.has(entity) && // + registry.has(entity) && // + registry.has(entity) && // + registry.has(entity); +} + +auto make_tileset(Registry& registry, const TilesetSpec& spec) -> EntityID +{ + const SetLogScope log_scope {"Tileset"}; + + if (spec.tile_size.x() <= 0 || spec.tile_size.y() <= 0) { + TACTILE_LOG_ERROR("Tried to create tileset with invalid tile size: {}", + fmt::streamed(spec.tile_size)); + return kInvalidEntity; + } + + const MatrixExtent extent { + .rows = static_cast(spec.texture.size.y() / spec.tile_size.y()), + .cols = static_cast(spec.texture.size.x() / spec.tile_size.x()), + }; + + if (extent.rows <= 0 || extent.cols <= 0) { + TACTILE_LOG_ERROR("Tried to create tileset with invalid extent: {}", + fmt::streamed(extent)); + return kInvalidEntity; + } + + const auto tileset_entity = registry.make_entity(); + + auto& tileset = registry.add(tileset_entity); + tileset.tile_size = spec.tile_size; + tileset.extent = extent; + + auto& viewport = registry.add(tileset_entity); + viewport.pos = Float2 {0.0f, 0.0f}; + viewport.size = vector_cast(spec.texture.size) * 0.5f; + viewport.scale = 1.0f; + + registry.add(tileset_entity); + registry.add(tileset_entity, spec.texture); + + _create_tiles(registry, tileset); + + TACTILE_ASSERT(is_tileset(registry, tileset_entity)); + TACTILE_ASSERT(tileset.extent.rows > 0); + TACTILE_ASSERT(tileset.extent.cols > 0); + return tileset_entity; +} + +auto init_tileset_instance(Registry& registry, + const EntityID tileset_entity, + const TileID first_tile_id) -> Result +{ + TACTILE_ASSERT(is_tileset(registry, tileset_entity)); + const SetLogScope log_scope {"Tileset"}; + + if (first_tile_id < TileID {1}) { + TACTILE_LOG_ERROR("Tried to use invalid first tile identifier: {}", + first_tile_id); + return unexpected(make_error(GenericError::kInvalidParam)); + } + + if (registry.has(tileset_entity)) { + TACTILE_LOG_ERROR("Tried to initialize tileset instance more than once"); + return unexpected(make_error(GenericError::kInvalidParam)); + } + + const auto& tileset = registry.get(tileset_entity); + + const TileRange tile_range { + .first_id = first_tile_id, + .count = narrow_cast(tileset.tiles.size()), + }; + + if (!is_tile_range_available(registry, tile_range)) { + TACTILE_LOG_ERROR("Requested tile range is unavailable: [{}, {})", + tile_range.first_id, + tile_range.first_id + tile_range.count); + return unexpected(make_error(GenericError::kInvalidParam)); + } + + auto& instance = registry.add(tileset_entity); + instance.tile_range = tile_range; + instance.is_embedded = false; // TODO + + auto& tile_cache = registry.get(); + tile_cache.tileset_mapping.reserve(tile_cache.tileset_mapping.size() + + tileset.tiles.size()); + + for (int32 index = 0; index < tile_range.count; ++index) { + const TileID tile_id {tile_range.first_id + index}; + tile_cache.tileset_mapping.insert_or_assign(tile_id, tileset_entity); + } + + TACTILE_LOG_DEBUG("Initialized tileset instance with tile range [{}, {})", + tile_range.first_id, + tile_range.first_id + tile_range.count); + return kOK; +} + +void destroy_tileset(Registry& registry, const EntityID tileset_entity) +{ + TACTILE_ASSERT(is_tileset(registry, tileset_entity)); + const auto& tileset = registry.get(tileset_entity); + + for (const auto tile_entity : tileset.tiles) { + destroy_tile(registry, tile_entity); + } + + if (const auto* instance = registry.find(tileset_entity)) { + auto& tile_cache = registry.get(); + for (int32 index = 0; index < instance->tile_range.count; ++index) { + const TileID tile_id {instance->tile_range.first_id + index}; + tile_cache.tileset_mapping.erase(tile_id); + } + } + + registry.destroy(tileset_entity); +} + +auto copy_tileset(Registry& registry, + const EntityID old_tileset_entity) -> EntityID +{ + TACTILE_ASSERT(is_tileset(registry, old_tileset_entity)); + const auto& old_meta = registry.get(old_tileset_entity); + const auto& old_tileset = registry.get(old_tileset_entity); + + const auto new_tileset_entity = registry.make_entity(); + + registry.add(new_tileset_entity, old_meta); + + auto& new_tileset = registry.add(new_tileset_entity); + new_tileset.extent = old_tileset.extent; + new_tileset.tile_size = old_tileset.tile_size; + + new_tileset.tiles.reserve(old_tileset.tiles.size()); + for (const auto old_tile_entity : old_tileset.tiles) { + new_tileset.tiles.push_back(copy_tile(registry, old_tile_entity)); + } + + if (const auto* instance = + registry.find(old_tileset_entity)) { + registry.add(new_tileset_entity, *instance); + } + + return new_tileset_entity; +} + +auto get_tile_appearance(const Registry& registry, + const EntityID tileset_entity, + const TileIndex tile_index) -> TileIndex +{ + TACTILE_ASSERT(is_tileset(registry, tileset_entity)); + const auto& tileset = registry.get(tileset_entity); + + const auto tile_entity = tileset.tiles.at(static_cast(tile_index)); + if (const auto* animation = registry.find(tile_entity)) { + return animation->frames.at(animation->frame_index).tile_index; + } + + return tile_index; +} + +auto find_tileset(const Registry& registry, const TileID tile_id) -> EntityID +{ + TACTILE_ASSERT(registry.has()); + const auto& tile_cache = registry.get(); + + const auto* tileset_entity = find_in(tile_cache.tileset_mapping, tile_id); + if (tileset_entity != nullptr) { + return *tileset_entity; + } + + return kInvalidEntity; +} + +auto get_tile_index(const Registry& registry, + const TileID tile_id) -> Maybe +{ + TACTILE_ASSERT(registry.has()); + + const auto tileset_entity = find_tileset(registry, tile_id); + + const auto* instance = registry.find(tileset_entity); + if (instance != nullptr && has_tile(instance->tile_range, tile_id)) { + return TileIndex {tile_id - instance->tile_range.first_id}; + } + + return kNone; +} + +auto is_tile_range_available(const Registry& registry, + const TileRange& range) -> bool +{ + const auto first_tile = range.first_id; + const auto last_tile = range.first_id + range.count - 1; + + if (first_tile < TileID {1}) { + return false; + } + + for (const auto& [tileset_entity, instance] : + registry.each()) { + if (has_tile(instance.tile_range, first_tile) || + has_tile(instance.tile_range, last_tile)) { + return false; + } + } + + return true; +} + +auto has_tile(const TileRange& tile_range, const TileID tile_id) -> bool +{ + return (tile_id >= tile_range.first_id) && + (tile_id < tile_range.first_id + tile_range.count); +} + +} // namespace tactile diff --git a/source/core/test/src/tile/tileset_test.cpp b/source/core/test/src/tile/tileset_test.cpp new file mode 100644 index 0000000000..be585fa519 --- /dev/null +++ b/source/core/test/src/tile/tileset_test.cpp @@ -0,0 +1,381 @@ +// Copyright (C) 2024 Albin Johansson (GNU General Public License v3.0) + +#include "tactile/core/tile/tileset.hpp" + +#include + +#include "tactile/core/entity/registry.hpp" +#include "tactile/core/io/texture.hpp" +#include "tactile/core/meta/meta.hpp" +#include "tactile/core/numeric/narrow.hpp" +#include "tactile/core/tile/animation.hpp" +#include "tactile/core/tile/tile.hpp" +#include "tactile/core/tile/tileset_spec.hpp" +#include "tactile/core/ui/viewport.hpp" + +namespace tactile { + +class TilesetTest : public testing::Test +{ + public: + TilesetTest() + { + mRegistry.add(); + } + + [[nodiscard]] + static auto make_dummy_texture(const Int2 size) -> CTexture + { + CTexture texture {}; + + texture.texture_uuid = UUID::generate(); + texture.size = size; + texture.path = "foo/bar.png"; + + return texture; + } + + [[nodiscard]] + auto make_dummy_tileset_with_100_tiles() -> EntityID + { + const auto texture = make_dummy_texture(Int2 {100, 100}); + + const TilesetSpec spec { + .tile_size = Int2 {10, 10}, + .texture = texture, + }; + + return make_tileset(mRegistry, spec); + } + + protected: + Registry mRegistry {}; +}; + +/// \trace tactile::make_tileset +TEST_F(TilesetTest, MakeTileset) +{ + const TilesetSpec spec { + .tile_size = Int2 {50, 30}, + .texture = make_dummy_texture(Int2(600, 330)) + }; + + const auto ts_entity = make_tileset(mRegistry, spec); + + EXPECT_TRUE(mRegistry.is_valid(ts_entity)); + EXPECT_TRUE(is_tileset(mRegistry, ts_entity)); + + const auto& meta = mRegistry.get(ts_entity); + const auto& tileset = mRegistry.get(ts_entity); + const auto& texture = mRegistry.get(ts_entity); + const auto& viewport = mRegistry.get(ts_entity); + + EXPECT_EQ(meta.name, ""); + EXPECT_EQ(meta.properties.size(), 0); + EXPECT_EQ(meta.components.size(), 0); + + EXPECT_EQ(tileset.tile_size, spec.tile_size); + EXPECT_EQ(tileset.extent.rows, spec.texture.size.y() / spec.tile_size.y()); + EXPECT_EQ(tileset.extent.cols, spec.texture.size.x() / spec.tile_size.x()); + EXPECT_EQ(tileset.tiles.size(), 132); + EXPECT_EQ(tileset.tiles.size(), tileset.extent.rows * tileset.extent.cols); + + for (const auto tile_entity : tileset.tiles) { + EXPECT_TRUE(mRegistry.is_valid(ts_entity)); + EXPECT_TRUE(is_tile(mRegistry, tile_entity)); + } + + EXPECT_EQ(texture.texture_uuid, spec.texture.texture_uuid); + EXPECT_EQ(texture.size, spec.texture.size); + EXPECT_EQ(texture.path, spec.texture.path); + + EXPECT_GT(viewport.size.x(), 0.0f); + EXPECT_GT(viewport.size.y(), 0.0f); + EXPECT_EQ(viewport.pos.x(), 0.0f); + EXPECT_EQ(viewport.pos.y(), 0.0f); +} + +/// \trace tactile::init_tileset_instance +TEST_F(TilesetTest, InitTilesetInstance) +{ + const auto& tile_cache = mRegistry.get(); + ASSERT_EQ(tile_cache.tileset_mapping.size(), 0); + + const auto ts_entity = make_dummy_tileset_with_100_tiles(); + ASSERT_FALSE(mRegistry.has(ts_entity)); + + // [42, 142) + const TileID first_tile {42}; + ASSERT_EQ(init_tileset_instance(mRegistry, ts_entity, first_tile), kOK); + ASSERT_TRUE(mRegistry.has(ts_entity)); + + const auto& tileset = mRegistry.get(ts_entity); + const auto& instance = mRegistry.get(ts_entity); + + EXPECT_EQ(instance.tile_range.first_id, first_tile); + EXPECT_EQ(instance.tile_range.count, + narrow_cast(tileset.tiles.size())); + EXPECT_FALSE(instance.is_embedded); + + EXPECT_EQ(tile_cache.tileset_mapping.size(), tileset.tiles.size()); + + EXPECT_FALSE(tile_cache.tileset_mapping.contains(first_tile - TileID {1})); + EXPECT_FALSE(tile_cache.tileset_mapping.contains(first_tile + + instance.tile_range.count)); + + for (int32 index = 0; index < instance.tile_range.count; ++index) { + const TileID tile_id {instance.tile_range.first_id + index}; + EXPECT_TRUE(tile_cache.tileset_mapping.contains(tile_id)); + } +} + +/// \trace tactile::init_tileset_instance +TEST_F(TilesetTest, InitTilesetInstanceWithInvalidTileIdentifier) +{ + const auto ts_entity = make_dummy_tileset_with_100_tiles(); + EXPECT_NE(init_tileset_instance(mRegistry, ts_entity, kEmptyTile), kOK); + EXPECT_NE(init_tileset_instance(mRegistry, ts_entity, TileID {-1}), kOK); + EXPECT_EQ(mRegistry.count(), 0); +} + +/// \trace tactile::init_tileset_instance +TEST_F(TilesetTest, InitTilesetInstanceMoreThanOnce) +{ + const auto ts_entity = make_dummy_tileset_with_100_tiles(); + EXPECT_EQ(init_tileset_instance(mRegistry, ts_entity, TileID {1}), kOK); + EXPECT_NE(init_tileset_instance(mRegistry, ts_entity, TileID {101}), kOK); +} + +/// \trace tactile::init_tileset_instance +TEST_F(TilesetTest, InitTilesetInstanceTileRangeCollisionDetection) +{ + const auto& tile_cache = mRegistry.get(); + ASSERT_EQ(tile_cache.tileset_mapping.size(), 0); + + const auto ts1_entity = make_dummy_tileset_with_100_tiles(); + const auto ts2_entity = make_dummy_tileset_with_100_tiles(); + + // [1, 101) + EXPECT_EQ(init_tileset_instance(mRegistry, ts1_entity, TileID {1}), kOK); + + // These tile ranges would overlap with the existing tileset. + EXPECT_NE(init_tileset_instance(mRegistry, ts2_entity, TileID {1}), kOK); + EXPECT_NE(init_tileset_instance(mRegistry, ts2_entity, TileID {50}), kOK); + EXPECT_NE(init_tileset_instance(mRegistry, ts2_entity, TileID {100}), kOK); + + // [101, 201) + EXPECT_EQ(init_tileset_instance(mRegistry, ts2_entity, TileID {101}), kOK); +} + +/// \trace tactile::destroy_tileset +TEST_F(TilesetTest, DestroyTileset) +{ + const auto ts_entity = make_dummy_tileset_with_100_tiles(); + + EXPECT_TRUE(mRegistry.is_valid(ts_entity)); + EXPECT_EQ(mRegistry.count(), 101); + EXPECT_EQ(mRegistry.count(), 1); + EXPECT_EQ(mRegistry.count(), 0); + EXPECT_EQ(mRegistry.count(), 1); + EXPECT_EQ(mRegistry.count(), 100); + EXPECT_GT(mRegistry.count(), 0); + + destroy_tileset(mRegistry, ts_entity); + + EXPECT_FALSE(mRegistry.is_valid(ts_entity)); + EXPECT_EQ(mRegistry.count(), 0); + EXPECT_EQ(mRegistry.count(), 0); + EXPECT_EQ(mRegistry.count(), 0); + EXPECT_EQ(mRegistry.count(), 0); + EXPECT_EQ(mRegistry.count(), 0); + EXPECT_EQ(mRegistry.count(), 0); +} + +/// \trace tactile::destroy_tileset +TEST_F(TilesetTest, DestroyTilesetInstance) +{ + const auto ts_entity = make_dummy_tileset_with_100_tiles(); + + const TileID first_tile {25}; + ASSERT_EQ(init_tileset_instance(mRegistry, ts_entity, first_tile), kOK); + + const auto& tile_cache = mRegistry.get(); + + EXPECT_TRUE(mRegistry.is_valid(ts_entity)); + EXPECT_EQ(mRegistry.count(), 101); + EXPECT_EQ(mRegistry.count(), 1); + EXPECT_EQ(mRegistry.count(), 1); + EXPECT_EQ(mRegistry.count(), 1); + EXPECT_EQ(mRegistry.count(), 100); + EXPECT_GT(mRegistry.count(), 0); + EXPECT_EQ(tile_cache.tileset_mapping.size(), 100); + + destroy_tileset(mRegistry, ts_entity); + + EXPECT_FALSE(mRegistry.is_valid(ts_entity)); + EXPECT_EQ(mRegistry.count(), 0); + EXPECT_EQ(mRegistry.count(), 0); + EXPECT_EQ(mRegistry.count(), 0); + EXPECT_EQ(mRegistry.count(), 0); + EXPECT_EQ(mRegistry.count(), 0); + EXPECT_EQ(mRegistry.count(), 0); + EXPECT_EQ(tile_cache.tileset_mapping.size(), 0); +} + +/// \trace tactile::get_tile_appearance +TEST_F(TilesetTest, GetTileAppearance) +{ + const auto ts_entity = make_dummy_tileset_with_100_tiles(); + const auto& tileset = mRegistry.get(ts_entity); + + const TileIndex index10 {10}; + const TileIndex index11 {11}; + const TileIndex index12 {12}; + + EXPECT_EQ(get_tile_appearance(mRegistry, ts_entity, index10), index10); + EXPECT_EQ(get_tile_appearance(mRegistry, ts_entity, index11), index11); + EXPECT_EQ(get_tile_appearance(mRegistry, ts_entity, index12), index12); + + const auto tile10_entity = tileset.tiles.at(10); + ASSERT_EQ(add_animation_frame(mRegistry, + tile10_entity, + 0, + AnimationFrame {index10, Milliseconds::zero()}), + kOK); + ASSERT_EQ(add_animation_frame(mRegistry, + tile10_entity, + 1, + AnimationFrame {index11, Milliseconds::zero()}), + kOK); + ASSERT_EQ(add_animation_frame(mRegistry, + tile10_entity, + 2, + AnimationFrame {index12, Milliseconds::zero()}), + kOK); + + EXPECT_EQ(get_tile_appearance(mRegistry, ts_entity, index10), index10); + EXPECT_EQ(get_tile_appearance(mRegistry, ts_entity, index11), index11); + EXPECT_EQ(get_tile_appearance(mRegistry, ts_entity, index12), index12); + + update_animations(mRegistry); + + EXPECT_EQ(get_tile_appearance(mRegistry, ts_entity, index10), index11); + EXPECT_EQ(get_tile_appearance(mRegistry, ts_entity, index11), index11); + EXPECT_EQ(get_tile_appearance(mRegistry, ts_entity, index12), index12); + + update_animations(mRegistry); + + EXPECT_EQ(get_tile_appearance(mRegistry, ts_entity, index10), index12); + EXPECT_EQ(get_tile_appearance(mRegistry, ts_entity, index11), index11); + EXPECT_EQ(get_tile_appearance(mRegistry, ts_entity, index12), index12); + + update_animations(mRegistry); + + EXPECT_EQ(get_tile_appearance(mRegistry, ts_entity, index10), index10); + EXPECT_EQ(get_tile_appearance(mRegistry, ts_entity, index11), index11); + EXPECT_EQ(get_tile_appearance(mRegistry, ts_entity, index12), index12); +} + +/// \trace tactile::find_tileset +TEST_F(TilesetTest, FindTileset) +{ + EXPECT_EQ(find_tileset(mRegistry, kEmptyTile), kInvalidEntity); + EXPECT_EQ(find_tileset(mRegistry, TileID {1}), kInvalidEntity); + + const auto ts1_entity = make_dummy_tileset_with_100_tiles(); + const auto ts2_entity = make_dummy_tileset_with_100_tiles(); + const auto ts3_entity = make_dummy_tileset_with_100_tiles(); + + // [1, 101) + ASSERT_EQ(init_tileset_instance(mRegistry, ts1_entity, TileID {1}), kOK); + + // [101, 201) + ASSERT_EQ(init_tileset_instance(mRegistry, ts2_entity, TileID {101}), kOK); + + // [201, 301) + ASSERT_EQ(init_tileset_instance(mRegistry, ts3_entity, TileID {201}), kOK); + + EXPECT_EQ(find_tileset(mRegistry, TileID {001}), ts1_entity); + EXPECT_EQ(find_tileset(mRegistry, TileID {100}), ts1_entity); + EXPECT_EQ(find_tileset(mRegistry, TileID {101}), ts2_entity); + EXPECT_EQ(find_tileset(mRegistry, TileID {200}), ts2_entity); + EXPECT_EQ(find_tileset(mRegistry, TileID {201}), ts3_entity); + EXPECT_EQ(find_tileset(mRegistry, TileID {300}), ts3_entity); + + EXPECT_EQ(find_tileset(mRegistry, kEmptyTile), kInvalidEntity); + EXPECT_EQ(find_tileset(mRegistry, TileID {301}), kInvalidEntity); +} + +/// \trace tactile::get_tile_index +TEST_F(TilesetTest, GetTileIndex) +{ + EXPECT_EQ(get_tile_index(mRegistry, kEmptyTile), kNone); + EXPECT_EQ(get_tile_index(mRegistry, TileID {1}), kNone); + + const auto ts1_entity = make_dummy_tileset_with_100_tiles(); + const auto ts2_entity = make_dummy_tileset_with_100_tiles(); + + // [10, 110) + ASSERT_EQ(init_tileset_instance(mRegistry, ts1_entity, TileID {10}), kOK); + + // [110, 210) + ASSERT_EQ(init_tileset_instance(mRegistry, ts2_entity, TileID {110}), kOK); + + EXPECT_EQ(get_tile_index(mRegistry, TileID {10}), 0); + EXPECT_EQ(get_tile_index(mRegistry, TileID {53}), 43); + EXPECT_EQ(get_tile_index(mRegistry, TileID {109}), 99); + + EXPECT_EQ(get_tile_index(mRegistry, TileID {110}), 0); + EXPECT_EQ(get_tile_index(mRegistry, TileID {137}), 27); + EXPECT_EQ(get_tile_index(mRegistry, TileID {209}), 99); + + EXPECT_EQ(get_tile_index(mRegistry, TileID {9}), kNone); + EXPECT_EQ(get_tile_index(mRegistry, TileID {210}), kNone); +} + +/// \trace tactile::is_tile_range_available +TEST_F(TilesetTest, IsTileRangeAvailable) +{ + EXPECT_FALSE(is_tile_range_available(mRegistry, {TileID {-1}, 10})); + EXPECT_FALSE(is_tile_range_available(mRegistry, {TileID {0}, 100})); + EXPECT_TRUE(is_tile_range_available(mRegistry, {TileID {1}, 100})); + + // [5, 105) + const auto ts_entity = make_dummy_tileset_with_100_tiles(); + ASSERT_EQ(init_tileset_instance(mRegistry, ts_entity, TileID {5}), kOK); + + // Identical range + EXPECT_FALSE(is_tile_range_available(mRegistry, {TileID {5}, 100})); + + // At limit + EXPECT_TRUE(is_tile_range_available(mRegistry, {TileID {4}, 1})); + EXPECT_TRUE(is_tile_range_available(mRegistry, {TileID {105}, 1})); + + // Beyond occupied range + EXPECT_TRUE(is_tile_range_available(mRegistry, {TileID {1}, 3})); + EXPECT_TRUE(is_tile_range_available(mRegistry, {TileID {200}, 25})); + + // 1 tile overlap + EXPECT_FALSE(is_tile_range_available(mRegistry, {TileID {1}, 5})); + EXPECT_FALSE(is_tile_range_available(mRegistry, {TileID {-1}, 7})); + + // Large overlap + EXPECT_FALSE(is_tile_range_available(mRegistry, {TileID {1}, 20})); + EXPECT_FALSE(is_tile_range_available(mRegistry, {TileID {40}, 100})); +} + +/// \trace tactile::has_tile +TEST_F(TilesetTest, HasTile) +{ + // [82, 182) + const TileRange range {TileID {82}, 100}; + + EXPECT_FALSE(has_tile(range, kEmptyTile)); + EXPECT_FALSE(has_tile(range, TileID {81})); + EXPECT_TRUE(has_tile(range, TileID {82})); + EXPECT_TRUE(has_tile(range, TileID {123})); + EXPECT_TRUE(has_tile(range, TileID {181})); + EXPECT_FALSE(has_tile(range, TileID {182})); +} + +} // namespace tactile