diff --git a/source/core/inc/tactile/core/tile/tileset.hpp b/source/core/inc/tactile/core/tile/tileset.hpp index 601e54d17e..2fcf9b1dc7 100644 --- a/source/core/inc/tactile/core/tile/tileset.hpp +++ b/source/core/inc/tactile/core/tile/tileset.hpp @@ -18,6 +18,11 @@ namespace tactile { struct CTexture; struct TilesetSpec; class Registry; +class IRenderer; + +namespace ir { +struct TilesetRef; +} // namespace ir /** * Represents an sequence of tile identifiers. @@ -104,6 +109,23 @@ auto is_tileset(const Registry& registry, EntityID entity) -> bool; [[nodiscard]] auto make_tileset(Registry& registry, const TilesetSpec& spec) -> EntityID; +/** + * Creates a tileset instance from an intermediate representation. + * + * \pre The registry must feature a \c CTileCache context component. + * + * \param registry The associated registry. + * \param renderer The renderer to use for loading textures. + * \param ir_tileset_ref The intermediate tileset representation. + * + * \return + * A tileset entity identifier if successful; an error code otherwise. + */ +[[nodiscard]] +auto make_tileset(Registry& registry, + IRenderer& renderer, + const ir::TilesetRef& ir_tileset_ref) -> Result; + /** * Initializes a tileset "instance". * @@ -223,8 +245,7 @@ auto find_tileset(const Registry& registry, TileID tile_id) -> EntityID; * A tile index if successful; nothing otherwise. */ [[nodiscard]] -auto get_tile_index(const Registry& registry, - TileID tile_id) -> Maybe; +auto get_tile_index(const Registry& registry, TileID tile_id) -> Maybe; /** * Indicates whether the tiles in a tile range are available for use. @@ -236,8 +257,7 @@ auto get_tile_index(const Registry& registry, * True if the tile range is available; false otherwise. */ [[nodiscard]] -auto is_tile_range_available(const Registry& registry, - const TileRange& range) -> bool; +auto is_tile_range_available(const Registry& registry, const TileRange& range) -> bool; /** * Indicates whether a tile range contains a given tile identifier. diff --git a/source/core/src/tactile/core/tile/tileset.cpp b/source/core/src/tactile/core/tile/tileset.cpp index d9e6ae7511..d38d0c1d0a 100644 --- a/source/core/src/tactile/core/tile/tileset.cpp +++ b/source/core/src/tactile/core/tile/tileset.cpp @@ -2,6 +2,10 @@ #include "tactile/core/tile/tileset.hpp" +#include // make_error_code, errc +#include // move + +#include "tactile/base/io/save/ir.hpp" #include "tactile/base/numeric/saturate_cast.hpp" #include "tactile/base/numeric/vec_format.hpp" #include "tactile/core/debug/assert.hpp" @@ -18,9 +22,49 @@ #include "tactile/core/util/lookup.hpp" namespace tactile { -inline namespace tileset { +namespace tileset { -void _create_tiles(Registry& registry, CTileset& tileset) +[[nodiscard]] +auto add_tileset_component(Registry& registry, + const EntityID tileset_id, + const Int2& texture_size, + const Int2& tile_size) -> Result +{ + if (tile_size.x() <= 0 || tile_size.y() <= 0) { + TACTILE_LOG_ERROR("Invalid tileset tile size: {}", tile_size); + return unexpected(std::make_error_code(std::errc::invalid_argument)); + } + + const MatrixExtent extent { + .rows = static_cast(texture_size.y() / tile_size.y()), + .cols = static_cast(texture_size.x() / tile_size.x()), + }; + + if (extent.rows <= 0 || extent.cols <= 0) { + TACTILE_LOG_ERROR("Invalid tileset extent: {}", extent); + return unexpected(std::make_error_code(std::errc::invalid_argument)); + } + + const auto tileset_entity = registry.make_entity(); + + auto& tileset = registry.add(tileset_id); + tileset.tile_size = tile_size; + tileset.extent = extent; + + return kOK; +} + +void add_viewport_component(Registry& registry, + const EntityID tileset_id, + const Int2& texture_size) +{ + auto& viewport = registry.add(tileset_id); + viewport.pos = Float2 {0.0f, 0.0f}; + viewport.size = vec_cast(texture_size) * 0.5f; + viewport.scale = 1.0f; +} + +void create_tiles(Registry& registry, CTileset& tileset) { const auto tile_count = tileset.extent.rows * tileset.extent.cols; tileset.tiles.reserve(saturate_cast(tile_count)); @@ -33,6 +77,39 @@ void _create_tiles(Registry& registry, CTileset& tileset) } } +[[nodiscard]] +auto create_tiles(Registry& registry, + CTileset& tileset, + const ir::Tileset& ir_tileset) -> Result +{ + tileset.tiles.resize(saturate_cast(ir_tileset.tile_count), kInvalidEntity); + + for (const auto& ir_tile : ir_tileset.tiles) { + const auto tile_id = make_tile(registry, ir_tile); + if (!tile_id.has_value()) { + return propagate_unexpected(tile_id); + } + + const auto tile_index = saturate_cast(ir_tile.index); + if (tile_index >= tileset.tiles.size()) { + return unexpected(std::make_error_code(std::errc::result_out_of_range)); + } + + tileset.tiles[tile_index] = *tile_id; + } + + TileIndex tile_index {0}; + for (auto& tile_id : tileset.tiles) { + if (tile_id == kInvalidEntity) { + tile_id = make_tile(registry, tile_index); + } + + ++tile_index; + } + + return kOK; +} + } // namespace tileset auto is_tileset(const Registry& registry, const EntityID entity) -> bool @@ -45,47 +122,74 @@ auto is_tileset(const Registry& registry, const EntityID entity) -> bool auto make_tileset(Registry& registry, const TilesetSpec& spec) -> EntityID { - const SetLogScope log_scope {"Tileset"}; + const auto tileset_id = registry.make_entity(); - if (spec.tile_size.x() <= 0 || spec.tile_size.y() <= 0) { - TACTILE_LOG_ERROR("Tried to create tileset with invalid tile size: {}", - spec.tile_size); + const auto add_tileset_result = + tileset::add_tileset_component(registry, tileset_id, spec.texture.size, spec.tile_size); + if (!add_tileset_result.has_value()) { 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()), - }; + tileset::add_viewport_component(registry, tileset_id, spec.texture.size); - if (extent.rows <= 0 || extent.cols <= 0) { - TACTILE_LOG_ERROR("Tried to create tileset with invalid extent: {}", - extent); + registry.add(tileset_id); + registry.add(tileset_id, spec.texture); + + auto& tileset = registry.get(tileset_id); + tileset::create_tiles(registry, tileset); + + TACTILE_ASSERT(is_tileset(registry, tileset_id)); + TACTILE_ASSERT(tileset.extent.rows > 0); + TACTILE_ASSERT(tileset.extent.cols > 0); + return tileset_id; +} + +auto make_tileset(Registry& registry, + IRenderer& renderer, + const ir::TilesetRef& ir_tileset_ref) -> Result +{ + auto texture_result = load_texture(renderer, ir_tileset_ref.tileset.image_path); + if (!texture_result.has_value()) { + return propagate_unexpected(texture_result); + } + + const auto tileset_id = registry.make_entity(); + registry.add(tileset_id); + + auto& texture = registry.add(tileset_id, std::move(*texture_result)); + + tileset::add_viewport_component(registry, tileset_id, texture.size); + + const auto add_tileset_result = + tileset::add_tileset_component(registry, + tileset_id, + texture.size, + ir_tileset_ref.tileset.tile_size); + if (!add_tileset_result.has_value()) { return kInvalidEntity; } - const auto tileset_entity = registry.make_entity(); + auto& tileset = registry.get(tileset_id); - auto& tileset = registry.add(tileset_entity); - tileset.tile_size = spec.tile_size; - tileset.extent = extent; + const auto create_tiles_result = + tileset::create_tiles(registry, tileset, ir_tileset_ref.tileset); + if (!create_tiles_result.has_value()) { + return propagate_unexpected(create_tiles_result); + } - auto& viewport = registry.add(tileset_entity); - viewport.pos = Float2 {0.0f, 0.0f}; - viewport.size = vec_cast(spec.texture.size) * 0.5f; - viewport.scale = 1.0f; + const auto init_instance_result = + init_tileset_instance(registry, tileset_id, ir_tileset_ref.first_tile_id); + if (!init_instance_result.has_value()) { + return propagate_unexpected(init_instance_result); + } - registry.add(tileset_entity); - registry.add(tileset_entity, spec.texture); + auto& tileset_instance = registry.get(tileset_id); + tileset_instance.is_embedded = ir_tileset_ref.tileset.is_embedded; - _create_tiles(registry, tileset); + convert_ir_metadata(registry, tileset_id, ir_tileset_ref.tileset.meta); - TACTILE_ASSERT(is_tileset(registry, tileset_entity)); - TACTILE_ASSERT(tileset.extent.rows > 0); - TACTILE_ASSERT(tileset.extent.cols > 0); - return tileset_entity; + TACTILE_ASSERT(is_tileset(registry, tileset_id)); + return tileset_id; } auto init_tileset_instance(Registry& registry, @@ -96,8 +200,7 @@ auto init_tileset_instance(Registry& registry, const SetLogScope log_scope {"Tileset"}; if (first_tile_id < TileID {1}) { - TACTILE_LOG_ERROR("Tried to use invalid first tile identifier: {}", - first_tile_id); + TACTILE_LOG_ERROR("Tried to use invalid first tile identifier: {}", first_tile_id); return unexpected(make_error(GenericError::kInvalidParam)); } @@ -125,8 +228,7 @@ auto init_tileset_instance(Registry& registry, instance.is_embedded = false; // TODO auto& tile_cache = registry.get(); - tile_cache.tileset_mapping.reserve(tile_cache.tileset_mapping.size() + - tileset.tiles.size()); + 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}; @@ -159,8 +261,7 @@ void destroy_tileset(Registry& registry, const EntityID tileset_entity) registry.destroy(tileset_entity); } -auto copy_tileset(Registry& registry, - const EntityID old_tileset_entity) -> EntityID +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); @@ -179,8 +280,7 @@ auto copy_tileset(Registry& registry, new_tileset.tiles.push_back(copy_tile(registry, old_tile_entity)); } - if (const auto* instance = - registry.find(old_tileset_entity)) { + if (const auto* instance = registry.find(old_tileset_entity)) { registry.add(new_tileset_entity, *instance); } @@ -215,8 +315,7 @@ auto find_tileset(const Registry& registry, const TileID tile_id) -> EntityID return kInvalidEntity; } -auto get_tile_index(const Registry& registry, - const TileID tile_id) -> Maybe +auto get_tile_index(const Registry& registry, const TileID tile_id) -> Maybe { TACTILE_ASSERT(registry.has()); @@ -230,8 +329,7 @@ auto get_tile_index(const Registry& registry, return kNone; } -auto is_tile_range_available(const Registry& registry, - const TileRange& range) -> bool +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; @@ -240,8 +338,7 @@ auto is_tile_range_available(const Registry& registry, return false; } - for (const auto& [tileset_entity, instance] : - registry.each()) { + 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; diff --git a/source/core/test/CMakeLists.txt b/source/core/test/CMakeLists.txt index 3a34a22f5c..565a4aec95 100644 --- a/source/core/test/CMakeLists.txt +++ b/source/core/test/CMakeLists.txt @@ -59,5 +59,6 @@ target_link_libraries(tactile-core-test PRIVATE tactile::core tactile::base_test_util + tactile::null_renderer GTest::gtest ) diff --git a/source/core/test/inc/tactile/core/test/ir_comparison.hpp b/source/core/test/inc/tactile/core/test/ir_comparison.hpp index 51ea086c45..893e3170d6 100644 --- a/source/core/test/inc/tactile/core/test/ir_comparison.hpp +++ b/source/core/test/inc/tactile/core/test/ir_comparison.hpp @@ -28,4 +28,8 @@ void compare_layer(const Registry& registry, EntityID layer_id, const ir::Layer& void compare_tile(const Registry& registry, EntityID tile_id, const ir::Tile& ir_tile); +void compare_tileset(const Registry& registry, + EntityID tileset_id, + const ir::TilesetRef& ir_tileset_ref); + } // namespace tactile::test diff --git a/source/core/test/src/ir_comparison.cpp b/source/core/test/src/ir_comparison.cpp index 2cbea30a9a..bdcf01ea68 100644 --- a/source/core/test/src/ir_comparison.cpp +++ b/source/core/test/src/ir_comparison.cpp @@ -4,9 +4,10 @@ #include // find_if -#include #include +#include "tactile/base/numeric/saturate_cast.hpp" +#include "tactile/core/io/texture.hpp" #include "tactile/core/layer/group_layer.hpp" #include "tactile/core/layer/layer.hpp" #include "tactile/core/layer/object.hpp" @@ -15,6 +16,7 @@ #include "tactile/core/meta/meta.hpp" #include "tactile/core/tile/animation.hpp" #include "tactile/core/tile/tile.hpp" +#include "tactile/core/tile/tileset.hpp" namespace tactile::test { namespace ir_comparison { @@ -197,4 +199,54 @@ void compare_tile(const Registry& registry, const EntityID tile_id, const ir::Ti compare_meta(registry, tile_id, ir_tile.meta); } +void compare_tileset(const Registry& registry, + const EntityID tileset_id, + const ir::TilesetRef& ir_tileset_ref) +{ + ASSERT_TRUE(is_tileset(registry, tileset_id)); + + const auto& ir_tileset = ir_tileset_ref.tileset; + + const auto& tileset = registry.get(tileset_id); + const auto& tileset_instance = registry.get(tileset_id); + const auto& texture = registry.get(tileset_id); + + EXPECT_EQ(tileset_instance.tile_range.first_id, ir_tileset_ref.first_tile_id); + EXPECT_EQ(tileset_instance.tile_range.count, ir_tileset.tile_count); + EXPECT_EQ(tileset_instance.is_embedded, ir_tileset.is_embedded); + + EXPECT_EQ(tileset.tile_size, ir_tileset.tile_size); + EXPECT_EQ(tileset.extent.cols, ir_tileset.column_count); + + EXPECT_NE(texture.id, TextureID {0}); + EXPECT_EQ(texture.size, ir_tileset.image_size); + EXPECT_EQ(texture.path, ir_tileset.image_path); + + ASSERT_EQ(tileset.tiles.size(), ir_tileset.tile_count); + EXPECT_EQ(tileset.tiles.capacity(), ir_tileset.tile_count); + + const auto& tile_cache = registry.get(); + + for (usize index = 0, count = tileset.tiles.size(); index < count; ++index) { + const auto tile_id = tileset.tiles.at(index); + EXPECT_NE(tile_id, kInvalidEntity) << "tile #" << index << " is invalid"; + EXPECT_TRUE(is_tile(registry, tile_id)); + + const auto global_tile_id = + tileset_instance.tile_range.first_id + saturate_cast(index); + + ASSERT_TRUE(tile_cache.tileset_mapping.contains(global_tile_id)) + << "tile " << global_tile_id << " is not in tile cache"; + EXPECT_EQ(tile_cache.tileset_mapping.at(global_tile_id), tileset_id); + } + + for (const auto& ir_tile : ir_tileset.tiles) { + const auto tile_index = saturate_cast(ir_tile.index); + const auto tile_id = tileset.tiles.at(tile_index); + compare_tile(registry, tile_id, ir_tile); + } + + compare_meta(registry, tileset_id, ir_tileset.meta); +} + } // namespace tactile::test diff --git a/source/core/test/src/tile/tileset_test.cpp b/source/core/test/src/tile/tileset_test.cpp index 5c72ed2958..60e1129df0 100644 --- a/source/core/test/src/tile/tileset_test.cpp +++ b/source/core/test/src/tile/tileset_test.cpp @@ -5,15 +5,18 @@ #include #include "tactile/base/numeric/saturate_cast.hpp" +#include "tactile/base/test_util/ir_presets.hpp" #include "tactile/core/entity/registry.hpp" #include "tactile/core/io/texture.hpp" #include "tactile/core/meta/meta.hpp" +#include "tactile/core/test/ir_comparison.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/null_renderer/null_renderer.hpp" -namespace tactile { +namespace tactile::test { class TilesetTest : public testing::Test { @@ -50,9 +53,10 @@ class TilesetTest : public testing::Test protected: Registry mRegistry {}; + NullRenderer mRenderer {nullptr}; }; -/// \trace tactile::make_tileset +// tactile::make_tileset [Registry&, const TilesetSpec&] TEST_F(TilesetTest, MakeTileset) { const TilesetSpec spec {.tile_size = Int2 {50, 30}, @@ -94,7 +98,20 @@ TEST_F(TilesetTest, MakeTileset) EXPECT_EQ(viewport.pos.y(), 0.0f); } -/// \trace tactile::init_tileset_instance +// tactile::make_tileset [Registry&, IRenderer&, const ir::TilesetRef&] +TEST_F(TilesetTest, MakeTilesetFromIR) +{ + TileID next_tile_id {1000}; + ObjectID next_object_id {10}; + const auto ir_tileset_ref = make_complex_ir_tileset(next_tile_id, next_object_id); + + const auto tileset_id = make_tileset(mRegistry, mRenderer, ir_tileset_ref); + ASSERT_TRUE(tileset_id.has_value()); + + compare_tileset(mRegistry, *tileset_id, ir_tileset_ref); +} + +// tactile::init_tileset_instance TEST_F(TilesetTest, InitTilesetInstance) { const auto& tile_cache = mRegistry.get(); @@ -112,15 +129,13 @@ TEST_F(TilesetTest, InitTilesetInstance) const auto& instance = mRegistry.get(ts_entity); EXPECT_EQ(instance.tile_range.first_id, first_tile); - EXPECT_EQ(instance.tile_range.count, - saturate_cast(tileset.tiles.size())); + EXPECT_EQ(instance.tile_range.count, saturate_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)); + 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}; @@ -128,7 +143,7 @@ TEST_F(TilesetTest, InitTilesetInstance) } } -/// \trace tactile::init_tileset_instance +// tactile::init_tileset_instance TEST_F(TilesetTest, InitTilesetInstanceWithInvalidTileIdentifier) { const auto ts_entity = make_dummy_tileset_with_100_tiles(); @@ -137,7 +152,7 @@ TEST_F(TilesetTest, InitTilesetInstanceWithInvalidTileIdentifier) EXPECT_EQ(mRegistry.count(), 0); } -/// \trace tactile::init_tileset_instance +// tactile::init_tileset_instance TEST_F(TilesetTest, InitTilesetInstanceMoreThanOnce) { const auto ts_entity = make_dummy_tileset_with_100_tiles(); @@ -145,7 +160,7 @@ TEST_F(TilesetTest, InitTilesetInstanceMoreThanOnce) EXPECT_NE(init_tileset_instance(mRegistry, ts_entity, TileID {101}), kOK); } -/// \trace tactile::init_tileset_instance +// tactile::init_tileset_instance TEST_F(TilesetTest, InitTilesetInstanceTileRangeCollisionDetection) { const auto& tile_cache = mRegistry.get(); @@ -166,7 +181,7 @@ TEST_F(TilesetTest, InitTilesetInstanceTileRangeCollisionDetection) EXPECT_EQ(init_tileset_instance(mRegistry, ts2_entity, TileID {101}), kOK); } -/// \trace tactile::destroy_tileset +// tactile::destroy_tileset TEST_F(TilesetTest, DestroyTileset) { const auto ts_entity = make_dummy_tileset_with_100_tiles(); @@ -190,7 +205,7 @@ TEST_F(TilesetTest, DestroyTileset) EXPECT_EQ(mRegistry.count(), 0); } -/// \trace tactile::destroy_tileset +// tactile::destroy_tileset TEST_F(TilesetTest, DestroyTilesetInstance) { const auto ts_entity = make_dummy_tileset_with_100_tiles(); @@ -221,7 +236,7 @@ TEST_F(TilesetTest, DestroyTilesetInstance) EXPECT_EQ(tile_cache.tileset_mapping.size(), 0); } -/// \trace tactile::get_tile_appearance +// tactile::get_tile_appearance TEST_F(TilesetTest, GetTileAppearance) { const auto ts_entity = make_dummy_tileset_with_100_tiles(); @@ -275,7 +290,7 @@ TEST_F(TilesetTest, GetTileAppearance) EXPECT_EQ(get_tile_appearance(mRegistry, ts_entity, index12), index12); } -/// \trace tactile::find_tileset +// tactile::find_tileset TEST_F(TilesetTest, FindTileset) { EXPECT_EQ(find_tileset(mRegistry, kEmptyTile), kInvalidEntity); @@ -305,7 +320,7 @@ TEST_F(TilesetTest, FindTileset) EXPECT_EQ(find_tileset(mRegistry, TileID {301}), kInvalidEntity); } -/// \trace tactile::get_tile_index +// tactile::get_tile_index TEST_F(TilesetTest, GetTileIndex) { EXPECT_EQ(get_tile_index(mRegistry, kEmptyTile), kNone); @@ -332,7 +347,7 @@ TEST_F(TilesetTest, GetTileIndex) EXPECT_EQ(get_tile_index(mRegistry, TileID {210}), kNone); } -/// \trace tactile::is_tile_range_available +// tactile::is_tile_range_available TEST_F(TilesetTest, IsTileRangeAvailable) { EXPECT_FALSE(is_tile_range_available(mRegistry, {TileID {-1}, 10})); @@ -363,7 +378,7 @@ TEST_F(TilesetTest, IsTileRangeAvailable) EXPECT_FALSE(is_tile_range_available(mRegistry, {TileID {40}, 100})); } -/// \trace tactile::has_tile +// tactile::has_tile TEST_F(TilesetTest, HasTile) { // [82, 182) @@ -377,4 +392,4 @@ TEST_F(TilesetTest, HasTile) EXPECT_FALSE(has_tile(range, TileID {182})); } -} // namespace tactile +} // namespace tactile::test