From a77dc7a716b78a698c0326a0484576ad944a0941 Mon Sep 17 00:00:00 2001 From: Albin Johansson Date: Tue, 8 Oct 2024 21:08:59 +0200 Subject: [PATCH] Restore support for Godot 3 scene format --- CMakeLists.txt | 2 +- assets/lang/en.ini | 3 + assets/lang/en_GB.ini | 3 + assets/lang/sv.ini | 3 + source/base/lib/CMakeLists.txt | 2 + .../lib/inc/tactile/base/container/result.hpp | 16 + .../lib/inc/tactile/base/debug/error_code.hpp | 88 +++ .../inc/tactile/base/document/layer_view.hpp | 23 + .../inc/tactile/base/io/save/save_format.hpp | 17 +- .../lib/inc/tactile/base/numeric/literals.hpp | 21 + source/core/CMakeLists.txt | 2 + .../tactile/core/document/layer_view_impl.hpp | 6 + .../tactile/core/event/map_event_handler.hpp | 6 + .../inc/tactile/core/event/map_events.hpp | 20 +- source/core/inc/tactile/core/tile/tile.hpp | 2 +- .../core/ui/common/attribute_widgets.hpp | 56 +- .../core/ui/dialog/godot_export_dialog.hpp | 39 ++ .../inc/tactile/core/ui/i18n/string_id.hpp | 3 + .../inc/tactile/core/ui/widget_manager.hpp | 5 + .../tactile/core/document/layer_view_impl.cpp | 44 ++ .../tactile/core/event/map_event_handler.cpp | 47 ++ source/core/src/tactile/core/tile/tileset.cpp | 2 +- .../core/ui/common/attribute_widgets.cpp | 67 ++- .../core/ui/dialog/godot_export_dialog.cpp | 92 ++++ .../tactile/core/ui/i18n/language_parser.cpp | 3 + .../src/tactile/core/ui/menu/map_menu.cpp | 12 +- .../src/tactile/core/ui/widget_manager.cpp | 6 + source/godot_tscn_format/lib/CMakeLists.txt | 26 - .../lib/inc/tactile/godot_tscn_format/api.hpp | 11 - .../godot_tscn_format_plugin.hpp | 28 - .../lib/src/godot_tscn_format_plugin.cpp | 33 -- source/godot_tscn_format/test/CMakeLists.txt | 1 - .../godot_tscn}/CMakeLists.txt | 2 +- source/plugins/godot_tscn/lib/CMakeLists.txt | 35 ++ .../lib/inc/tactile/godot_tscn/api.hpp | 11 + .../godot_tscn/gd3_document_converter.hpp | 62 +++ .../inc/tactile/godot_tscn/gd3_exporter.hpp | 27 + .../tactile/godot_tscn/gd3_scene_writer.hpp | 288 ++++++++++ .../lib/inc/tactile/godot_tscn/gd3_types.hpp | 202 +++++++ .../tactile/godot_tscn/godot_scene_format.hpp | 35 ++ .../godot_tscn/godot_scene_format_plugin.hpp | 32 ++ .../lib/src/gd3_document_converter.cpp | 432 +++++++++++++++ .../godot_tscn/lib/src/gd3_exporter.cpp | 512 ++++++++++++++++++ .../godot_tscn/lib/src/gd3_scene_writer.cpp | 165 ++++++ .../godot_tscn/lib/src/godot_scene_format.cpp | 86 +++ .../lib/src/godot_scene_format_plugin.cpp | 41 ++ .../godot_tscn}/test/.clang-tidy | 0 source/plugins/godot_tscn/test/CMakeLists.txt | 1 + source/runtime/lib/CMakeLists.txt | 2 + .../inc/tactile/runtime/document_factory.hpp | 44 ++ .../lib/src/tactile/document_factory.cpp | 31 ++ .../tactile/test_util/document_view_mocks.hpp | 7 + 52 files changed, 2543 insertions(+), 161 deletions(-) create mode 100644 source/base/lib/inc/tactile/base/container/result.hpp create mode 100644 source/base/lib/inc/tactile/base/debug/error_code.hpp create mode 100644 source/base/lib/inc/tactile/base/numeric/literals.hpp create mode 100644 source/core/inc/tactile/core/ui/dialog/godot_export_dialog.hpp create mode 100644 source/core/src/tactile/core/ui/dialog/godot_export_dialog.cpp delete mode 100644 source/godot_tscn_format/lib/CMakeLists.txt delete mode 100644 source/godot_tscn_format/lib/inc/tactile/godot_tscn_format/api.hpp delete mode 100644 source/godot_tscn_format/lib/inc/tactile/godot_tscn_format/godot_tscn_format_plugin.hpp delete mode 100644 source/godot_tscn_format/lib/src/godot_tscn_format_plugin.cpp delete mode 100644 source/godot_tscn_format/test/CMakeLists.txt rename source/{godot_tscn_format => plugins/godot_tscn}/CMakeLists.txt (68%) create mode 100644 source/plugins/godot_tscn/lib/CMakeLists.txt create mode 100644 source/plugins/godot_tscn/lib/inc/tactile/godot_tscn/api.hpp create mode 100644 source/plugins/godot_tscn/lib/inc/tactile/godot_tscn/gd3_document_converter.hpp create mode 100644 source/plugins/godot_tscn/lib/inc/tactile/godot_tscn/gd3_exporter.hpp create mode 100644 source/plugins/godot_tscn/lib/inc/tactile/godot_tscn/gd3_scene_writer.hpp create mode 100644 source/plugins/godot_tscn/lib/inc/tactile/godot_tscn/gd3_types.hpp create mode 100644 source/plugins/godot_tscn/lib/inc/tactile/godot_tscn/godot_scene_format.hpp create mode 100644 source/plugins/godot_tscn/lib/inc/tactile/godot_tscn/godot_scene_format_plugin.hpp create mode 100644 source/plugins/godot_tscn/lib/src/gd3_document_converter.cpp create mode 100644 source/plugins/godot_tscn/lib/src/gd3_exporter.cpp create mode 100644 source/plugins/godot_tscn/lib/src/gd3_scene_writer.cpp create mode 100644 source/plugins/godot_tscn/lib/src/godot_scene_format.cpp create mode 100644 source/plugins/godot_tscn/lib/src/godot_scene_format_plugin.cpp rename source/{godot_tscn_format => plugins/godot_tscn}/test/.clang-tidy (100%) create mode 100644 source/plugins/godot_tscn/test/CMakeLists.txt create mode 100644 source/runtime/lib/inc/tactile/runtime/document_factory.hpp create mode 100644 source/runtime/lib/src/tactile/document_factory.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 961124f859..2c03a7a401 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -104,7 +104,7 @@ if (TACTILE_BUILD_TILED_TMX_FORMAT) endif () if (TACTILE_BUILD_GODOT_TSCN_FORMAT) - add_subdirectory("source/godot_tscn_format") + add_subdirectory("source/plugins/godot_tscn") endif () if (TACTILE_BUILD_ZLIB_COMPRESSION) diff --git a/assets/lang/en.ini b/assets/lang/en.ini index 489b6b00fa..647783280a 100644 --- a/assets/lang/en.ini +++ b/assets/lang/en.ini @@ -40,6 +40,8 @@ light_themes = Light themes dark_themes = Dark themes font = Font default = Default +version = Version +project_dir = Project directory [adjective] orthogonal = Orthogonal @@ -137,4 +139,5 @@ about_dialog = About Tactile credits_dialog = Credits create_map_dialog = Create Map create_tileset_dialog = Create Tileset +godot_export_dialog = Export Godot Scene style_editor = Style Editor diff --git a/assets/lang/en_GB.ini b/assets/lang/en_GB.ini index 5cb3efaad0..4bd04422b6 100644 --- a/assets/lang/en_GB.ini +++ b/assets/lang/en_GB.ini @@ -40,6 +40,8 @@ light_themes = Light themes dark_themes = Dark themes font = Font default = Default +version = Version +project_dir = Project directory [adjective] orthogonal = Orthogonal @@ -137,4 +139,5 @@ about_dialog = About Tactile credits_dialog = Credits create_map_dialog = Create Map create_tileset_dialog = Create Tileset +godot_export_dialog = Export Godot Scene style_editor = Style Editor diff --git a/assets/lang/sv.ini b/assets/lang/sv.ini index cb2740d1d0..2dae322f81 100644 --- a/assets/lang/sv.ini +++ b/assets/lang/sv.ini @@ -40,6 +40,8 @@ light_themes = Ljusa teman dark_themes = Mörka teman font = Typsnitt default = Standard +version = Version +project_dir = Projektmapp [adjective] orthogonal = Ortogonal @@ -137,4 +139,5 @@ about_dialog = Om Tactile credits_dialog = Tredjeparter create_map_dialog = Skapa Karta create_tileset_dialog = Skapa Tilesamling +godot_export_dialog = Exportera Godotscen style_editor = Stilhanterare diff --git a/source/base/lib/CMakeLists.txt b/source/base/lib/CMakeLists.txt index f38c6199bb..c8ec799895 100644 --- a/source/base/lib/CMakeLists.txt +++ b/source/base/lib/CMakeLists.txt @@ -5,6 +5,7 @@ add_library(tactile::base ALIAS tactile-base) target_sources(tactile-base INTERFACE FILE_SET "HEADERS" BASE_DIRS "inc" FILES + "inc/tactile/base/container/result.hpp" "inc/tactile/base/container/string_map.hpp" "inc/tactile/base/document/component_view.hpp" "inc/tactile/base/document/document.hpp" @@ -38,6 +39,7 @@ target_sources(tactile-base "inc/tactile/base/meta/color.hpp" "inc/tactile/base/numeric/extent_2d.hpp" "inc/tactile/base/numeric/index_2d.hpp" + "inc/tactile/base/numeric/literals.hpp" "inc/tactile/base/numeric/offset_2d.hpp" "inc/tactile/base/numeric/saturate_cast.hpp" "inc/tactile/base/numeric/vec.hpp" diff --git a/source/base/lib/inc/tactile/base/container/result.hpp b/source/base/lib/inc/tactile/base/container/result.hpp new file mode 100644 index 0000000000..b0dcece58b --- /dev/null +++ b/source/base/lib/inc/tactile/base/container/result.hpp @@ -0,0 +1,16 @@ +// Copyright (C) 2024 Albin Johansson (GNU General Public License v3.0) + +#pragma once + +#include // expected + +#include "tactile/base/debug/error_code.hpp" + +namespace tactile { + +template +using Result = std::expected; + +inline constexpr Result kOK {}; + +} // namespace tactile diff --git a/source/base/lib/inc/tactile/base/debug/error_code.hpp b/source/base/lib/inc/tactile/base/debug/error_code.hpp new file mode 100644 index 0000000000..1ef9c78b65 --- /dev/null +++ b/source/base/lib/inc/tactile/base/debug/error_code.hpp @@ -0,0 +1,88 @@ +// Copyright (C) 2024 Albin Johansson (GNU General Public License v3.0) + +#pragma once + +#include // string_view + +namespace tactile { + +/** + * Provides common error codes. + */ +enum class ErrorCode : int +{ + /** An unknown error occurred. */ + kUnknown, + + /** An operation or feature isn't supported. */ + kNotSupported, + + /** Not enough memory. */ + kOutOfMemory, + + /** A stack overflow was detected. */ + kStackOverflow, + + /** A stack underflow was detected. */ + kStackUnderflow, + + /** Initialization failed. */ + kBadInit, + + /** A given parameter is invalid. */ + kBadParam, + + /** An invalid state was detected. */ + kBadState, + + /** An invalid operation was attempted. */ + kBadOperation, + + /** A file doesn't exist. */ + kNoSuchFile, + + /** A file stream couldn't be created. */ + kBadFileStream, + + /** A file couldn't be copied. */ + kBadFileCopy, + + /** An invalid image was detected. */ + kBadImage, + + /** An invalid save file was detected. */ + kBadSaveFile, + + /** A compression operation failed. */ + kCouldNotCompress, + + /** A decompression operation failed. */ + kCouldNotDecompress, +}; + +[[nodiscard]] +constexpr auto to_string(const ErrorCode errc) noexcept -> std::string_view +{ + switch (errc) { + case ErrorCode::kUnknown: return "unknown"; + case ErrorCode::kNotSupported: return "not supported"; + case ErrorCode::kOutOfMemory: return "out of memory"; + case ErrorCode::kStackOverflow: return "stack overflow"; + case ErrorCode::kStackUnderflow: return "stack underflow"; + case ErrorCode::kBadInit: return "initialization error"; + case ErrorCode::kBadParam: return "invalid parameter"; + case ErrorCode::kBadState: return "invalid state"; + case ErrorCode::kBadOperation: return "invalid operation"; + case ErrorCode::kNoSuchFile: return "no such file"; + case ErrorCode::kBadFileStream: return "file stream error"; + case ErrorCode::kBadFileCopy: return "file copy error"; + case ErrorCode::kBadImage: return "invalid image"; + case ErrorCode::kBadSaveFile: return "invalid save file"; + case ErrorCode::kCouldNotCompress: return "could not compress"; + case ErrorCode::kCouldNotDecompress: return "could not decompress"; + } + + return "?"; +} + +} // namespace tactile diff --git a/source/base/lib/inc/tactile/base/document/layer_view.hpp b/source/base/lib/inc/tactile/base/document/layer_view.hpp index f88986d582..2b9eb47b40 100644 --- a/source/base/lib/inc/tactile/base/document/layer_view.hpp +++ b/source/base/lib/inc/tactile/base/document/layer_view.hpp @@ -129,6 +129,29 @@ class ILayerView [[nodiscard]] virtual auto get_tile(const Index2D& index) const -> std::optional = 0; + /** + * Returns the position of a tile in its parent tileset. + * + * \param tile_id The target tile identifier. + * + * \return + * The position of the tile in the tileset if successful; an empty optional otherwise. + */ + [[nodiscard]] + virtual auto get_tile_position_in_tileset(TileID tile_id) const + -> std::optional = 0; + + /** + * Indicates whether the tile at a given world position is animated. + * + * \param world_pos The world position of the tile to check. + * + * \return + * True if the tile is animated; false otherwise. + */ + [[nodiscard]] + virtual auto is_tile_animated(const Index2D& world_pos) const -> bool = 0; + /** * Returns the tile encoding format used by the layer. * diff --git a/source/base/lib/inc/tactile/base/io/save/save_format.hpp b/source/base/lib/inc/tactile/base/io/save/save_format.hpp index 08bc40c928..ee569ca3de 100644 --- a/source/base/lib/inc/tactile/base/io/save/save_format.hpp +++ b/source/base/lib/inc/tactile/base/io/save/save_format.hpp @@ -2,22 +2,30 @@ #pragma once -#include // expected -#include // path -#include // error_code +#include // expected +#include // path +#include // string_view +#include // error_code +#include // unordered_map #include "tactile/base/io/save/ir.hpp" +#include "tactile/base/meta/attribute.hpp" #include "tactile/base/prelude.hpp" namespace tactile { class IMapView; +using SaveFormatExtraSettings = std::unordered_map; + /** * Provides save format parse options. */ struct SaveFormatReadOptions final { + /** Used for implementation-specific settings. */ + SaveFormatExtraSettings extra; + /** The parent directory of the map or tileset file. */ std::filesystem::path base_dir; @@ -30,6 +38,9 @@ struct SaveFormatReadOptions final */ struct SaveFormatWriteOptions final { + /** Used for implementation-specific settings. */ + SaveFormatExtraSettings extra; + /** The parent directory of the map or tileset file. */ std::filesystem::path base_dir; diff --git a/source/base/lib/inc/tactile/base/numeric/literals.hpp b/source/base/lib/inc/tactile/base/numeric/literals.hpp new file mode 100644 index 0000000000..3a61f391c3 --- /dev/null +++ b/source/base/lib/inc/tactile/base/numeric/literals.hpp @@ -0,0 +1,21 @@ +// Copyright (C) 2024 Albin Johansson (GNU General Public License v3.0) + +#pragma once + +#include // size_t, ptrdiff_t + +namespace tactile { + +[[nodiscard]] +consteval auto operator""_uz(const unsigned long long int value) noexcept -> std::size_t +{ + return static_cast(value); +} + +[[nodiscard]] +consteval auto operator""_z(const unsigned long long int value) noexcept -> std::ptrdiff_t +{ + return static_cast(value); +} + +} // namespace tactile diff --git a/source/core/CMakeLists.txt b/source/core/CMakeLists.txt index 5028ee82e1..9bb93a78e6 100644 --- a/source/core/CMakeLists.txt +++ b/source/core/CMakeLists.txt @@ -101,6 +101,7 @@ target_sources(tactile-core "src/tactile/core/ui/common/style.cpp" "src/tactile/core/ui/common/widgets.cpp" "src/tactile/core/ui/common/window.cpp" + "src/tactile/core/ui/dialog/godot_export_dialog.cpp" "src/tactile/core/ui/dialog/new_map_dialog.cpp" "src/tactile/core/ui/dialog/new_property_dialog.cpp" "src/tactile/core/ui/dialog/new_tileset_dialog.cpp" @@ -237,6 +238,7 @@ target_sources(tactile-core "inc/tactile/core/ui/common/text.hpp" "inc/tactile/core/ui/common/widgets.hpp" "inc/tactile/core/ui/common/window.hpp" + "inc/tactile/core/ui/dialog/godot_export_dialog.hpp" "inc/tactile/core/ui/dialog/new_map_dialog.hpp" "inc/tactile/core/ui/dialog/new_property_dialog.hpp" "inc/tactile/core/ui/dialog/new_tileset_dialog.hpp" diff --git a/source/core/inc/tactile/core/document/layer_view_impl.hpp b/source/core/inc/tactile/core/document/layer_view_impl.hpp index e763d7a5b9..4911a79ca9 100644 --- a/source/core/inc/tactile/core/document/layer_view_impl.hpp +++ b/source/core/inc/tactile/core/document/layer_view_impl.hpp @@ -63,6 +63,12 @@ class LayerViewImpl final : public ILayerView [[nodiscard]] auto get_tile(const Index2D& index) const -> std::optional override; + [[nodiscard]] + auto get_tile_position_in_tileset(TileID tile_id) const -> std::optional override; + + [[nodiscard]] + auto is_tile_animated(const Index2D& position) const -> bool override; + [[nodiscard]] auto get_tile_encoding() const -> TileEncoding override; diff --git a/source/core/inc/tactile/core/event/map_event_handler.hpp b/source/core/inc/tactile/core/event/map_event_handler.hpp index 8c30fa97d4..110a16eed1 100644 --- a/source/core/inc/tactile/core/event/map_event_handler.hpp +++ b/source/core/inc/tactile/core/event/map_event_handler.hpp @@ -20,7 +20,9 @@ class WidgetManager; struct ShowNewMapDialogEvent; struct ShowOpenMapDialogEvent; +struct ShowGodotExportDialogEvent; struct CreateMapEvent; +struct ExportAsGodotSceneEvent; /** * Handles events related to maps. @@ -58,6 +60,8 @@ class MapEventHandler final */ void on_show_open_map_dialog(const ShowOpenMapDialogEvent& event); + void on_show_godot_export_dialog(const ShowGodotExportDialogEvent& event); + /** * Creates a new map. * @@ -65,6 +69,8 @@ class MapEventHandler final */ void on_create_map(const CreateMapEvent& event); + void on_export_as_godot_scene(const ExportAsGodotSceneEvent& event) const; + private: Model* mModel; ui::WidgetManager* mWidgetManager; diff --git a/source/core/inc/tactile/core/event/map_events.hpp b/source/core/inc/tactile/core/event/map_events.hpp index 086636987b..3120885be6 100644 --- a/source/core/inc/tactile/core/event/map_events.hpp +++ b/source/core/inc/tactile/core/event/map_events.hpp @@ -2,8 +2,8 @@ #pragma once -#include "tactile/base/io/save/save_format_id.hpp" -#include "tactile/base/prelude.hpp" +#include // path + #include "tactile/core/map/map_spec.hpp" namespace tactile { @@ -66,11 +66,21 @@ struct FixMapTilesEvent final {}; /** - * Event for opening the dialog for exporting the active map using a specific save format. + * Event for opening the dialog for exporting the active map as a Godot scene. + */ +struct ShowGodotExportDialogEvent final +{}; + +/** + * Event for saving the active map as a Godot scene. */ -struct ShowExportMapDialogEvent final +struct ExportAsGodotSceneEvent final { - SaveFormatId format_id; + /** The desired major Godot version. */ + int version; + + /** The base directory of the Godot project. */ + std::filesystem::path project_dir; }; } // namespace tactile diff --git a/source/core/inc/tactile/core/tile/tile.hpp b/source/core/inc/tactile/core/tile/tile.hpp index 1004cac1fb..f1f6cf8cc8 100644 --- a/source/core/inc/tactile/core/tile/tile.hpp +++ b/source/core/inc/tactile/core/tile/tile.hpp @@ -63,7 +63,7 @@ auto make_tile(Registry& registry, TileIndex index) -> EntityID; * Creates a tile from an intermediate representation. * * \param registry The associated registry. - * \param tile The intermediate tile representation. + * \param ir_tile The intermediate tile representation. * * \return * A tile entity identifier if successful; an error code otherwise. diff --git a/source/core/inc/tactile/core/ui/common/attribute_widgets.hpp b/source/core/inc/tactile/core/ui/common/attribute_widgets.hpp index fb0626d5a0..d0e29378d0 100644 --- a/source/core/inc/tactile/core/ui/common/attribute_widgets.hpp +++ b/source/core/inc/tactile/core/ui/common/attribute_widgets.hpp @@ -33,8 +33,8 @@ auto push_string_input(const char* id, const Attribute::string_type& str) * \return * The new value if modified; an empty optional otherwise. */ -auto push_int_input(const char* id, - Attribute::int_type value) -> std::optional; +auto push_int_input(const char* id, Attribute::int_type value) + -> std::optional; /** * Pushes an attribute 2D integer input to the widget stack. @@ -45,8 +45,8 @@ auto push_int_input(const char* id, * \return * The new value if modified; an empty optional otherwise. */ -auto push_int2_input(const char* id, - Attribute::int2_type value) -> std::optional; +auto push_int2_input(const char* id, Attribute::int2_type value) + -> std::optional; /** * Pushes an attribute 3D integer input to the widget stack. @@ -57,8 +57,8 @@ auto push_int2_input(const char* id, * \return * The new value if modified; an empty optional otherwise. */ -auto push_int3_input(const char* id, - Attribute::int3_type value) -> std::optional; +auto push_int3_input(const char* id, Attribute::int3_type value) + -> std::optional; /** * Pushes an attribute 4D integer input to the widget stack. @@ -69,8 +69,8 @@ auto push_int3_input(const char* id, * \return * The new value if modified; an empty optional otherwise. */ -auto push_int4_input(const char* id, - Attribute::int4_type value) -> std::optional; +auto push_int4_input(const char* id, Attribute::int4_type value) + -> std::optional; /** * Pushes an attribute float input to the widget stack. @@ -81,8 +81,8 @@ auto push_int4_input(const char* id, * \return * The new value if modified; an empty optional otherwise. */ -auto push_float_input(const char* id, - Attribute::float_type value) -> std::optional; +auto push_float_input(const char* id, Attribute::float_type value) + -> std::optional; /** * Pushes an attribute 2D float input to the widget stack. @@ -93,8 +93,8 @@ auto push_float_input(const char* id, * \return * The new value if modified; an empty optional otherwise. */ -auto push_float2_input(const char* id, - Attribute::float2_type value) -> std::optional; +auto push_float2_input(const char* id, Attribute::float2_type value) + -> std::optional; /** * Pushes an attribute 3D float input to the widget stack. @@ -105,8 +105,8 @@ auto push_float2_input(const char* id, * \return * The new value if modified; an empty optional otherwise. */ -auto push_float3_input(const char* id, - Attribute::float3_type value) -> std::optional; +auto push_float3_input(const char* id, Attribute::float3_type value) + -> std::optional; /** * Pushes an attribute 4D float input to the widget stack. @@ -117,8 +117,8 @@ auto push_float3_input(const char* id, * \return * The new value if modified; an empty optional otherwise. */ -auto push_float4_input(const char* id, - Attribute::float4_type value) -> std::optional; +auto push_float4_input(const char* id, Attribute::float4_type value) + -> std::optional; /** * Pushes an attribute boolean input to the widget stack. @@ -134,14 +134,26 @@ auto push_bool_input(const char* id, bool value) -> std::optional; /** * Pushes an attribute path input to the widget stack. * - * \param id The widget identifier. - * \param value The current path value. + * \param id The widget identifier. + * \param path The current path value. + * + * \return + * The new value if modified; an empty optional otherwise. + */ +auto push_path_input(const char* id, const Attribute::path_type& path) + -> std::optional; + +/** + * Pushes a directory path input to the widget stack. + * + * \param id The widget identifier. + * \param path The current directory path value. * * \return * The new value if modified; an empty optional otherwise. */ -auto push_path_input(const char* id, - const Attribute::path_type& path) -> std::optional; +auto push_directory_input(const char* id, const Attribute::path_type& path) + -> std::optional; /** * Pushes an attribute color input to the widget stack. @@ -176,8 +188,8 @@ auto push_objref_input(const char* id, const Attribute::objref_type& object) * \return * The new attribute value if modified; an empty optional otherwise. */ -auto push_attribute_input(const char* id, - const Attribute& attribute) -> std::optional; +auto push_attribute_input(const char* id, const Attribute& attribute) + -> std::optional; /** * Pushes a combo for selecting an attribute type to the widget stack. diff --git a/source/core/inc/tactile/core/ui/dialog/godot_export_dialog.hpp b/source/core/inc/tactile/core/ui/dialog/godot_export_dialog.hpp new file mode 100644 index 0000000000..358f91d454 --- /dev/null +++ b/source/core/inc/tactile/core/ui/dialog/godot_export_dialog.hpp @@ -0,0 +1,39 @@ +// Copyright (C) 2024 Albin Johansson (GNU General Public License v3.0) + +#pragma once + +#include // path + +namespace tactile { +class Model; +class EventDispatcher; +} // namespace tactile + +namespace tactile::ui { + +/** + * A dialog for exporting maps as Godot engine scenes. + */ +class GodotExportDialog final +{ + public: + /** + * Pushes the dialog to the widget stack. + * + * \param model The associated model. + * \param dispatcher The event dispatcher that will be used. + */ + void push(const Model& model, EventDispatcher& dispatcher); + + /** + * Schedules the dialog to be opened. + */ + void open(); + + private: + std::filesystem::path m_project_dir {}; + int m_version {}; + bool m_should_open {false}; +}; + +} // namespace tactile::ui diff --git a/source/core/inc/tactile/core/ui/i18n/string_id.hpp b/source/core/inc/tactile/core/ui/i18n/string_id.hpp index 870f8d7bee..f16c64e8a5 100644 --- a/source/core/inc/tactile/core/ui/i18n/string_id.hpp +++ b/source/core/inc/tactile/core/ui/i18n/string_id.hpp @@ -51,6 +51,8 @@ enum class StringID : std::size_t kDarkThemes, kFont, kDefault, + kVersion, + kProjectDir, // Generic verbs. kCancel, @@ -76,6 +78,7 @@ enum class StringID : std::size_t kAnimationDock, kLogDock, kStyleEditorWidget, + kGodotExportDialog, // Menu names. kFileMenu, diff --git a/source/core/inc/tactile/core/ui/widget_manager.hpp b/source/core/inc/tactile/core/ui/widget_manager.hpp index 9dde8321b7..5fd21fa2f3 100644 --- a/source/core/inc/tactile/core/ui/widget_manager.hpp +++ b/source/core/inc/tactile/core/ui/widget_manager.hpp @@ -3,6 +3,7 @@ #pragma once #include "tactile/base/prelude.hpp" +#include "tactile/core/ui/dialog/godot_export_dialog.hpp" #include "tactile/core/ui/dialog/new_map_dialog.hpp" #include "tactile/core/ui/dialog/new_property_dialog.hpp" #include "tactile/core/ui/dialog/new_tileset_dialog.hpp" @@ -55,6 +56,9 @@ class WidgetManager final [[nodiscard]] auto get_new_map_dialog() -> NewMapDialog&; + [[nodiscard]] + auto get_godot_export_dialog() -> GodotExportDialog&; + /** * Returns the tileset creation dialog. * @@ -95,6 +99,7 @@ class WidgetManager final NewTilesetDialog mNewTilesetDialog {}; NewPropertyDialog mNewPropertyDialog {}; RenamePropertyDialog mRenamePropertyDialog {}; + GodotExportDialog m_godot_export_dialog {}; }; } // namespace ui diff --git a/source/core/src/tactile/core/document/layer_view_impl.cpp b/source/core/src/tactile/core/document/layer_view_impl.cpp index 1c8c61b45f..8f4cb68962 100644 --- a/source/core/src/tactile/core/document/layer_view_impl.cpp +++ b/source/core/src/tactile/core/document/layer_view_impl.cpp @@ -15,6 +15,8 @@ #include "tactile/core/layer/tile_layer.hpp" #include "tactile/core/map/map.hpp" #include "tactile/core/meta/meta.hpp" +#include "tactile/core/tile/animation.hpp" +#include "tactile/core/tile/tileset.hpp" namespace tactile { @@ -161,6 +163,48 @@ auto LayerViewImpl::get_tile(const Index2D& index) const -> std::optional std::optional +{ + const auto& registry = mDocument->get_registry(); + + const auto tile_index = get_tile_index(registry, tile_id); + if (!tile_index.has_value()) { + return std::nullopt; + } + + const auto tileset_id = find_tileset(registry, tile_id); + if (tileset_id == kInvalidEntity) { + return std::nullopt; + } + + const auto& tileset = registry.get(tileset_id); + return Index2D::from_1d(static_cast(*tile_index), tileset.extent.cols); +} + +auto LayerViewImpl::is_tile_animated(const Index2D& position) const -> bool +{ + const auto& registry = mDocument->get_registry(); + + const auto tile_id = get_layer_tile(registry, mLayerId, position).value(); + if (tile_id == kEmptyTile) { + return false; + } + + const auto tileset_id = find_tileset(registry, tile_id); + if (tileset_id == kInvalidEntity) { + return false; + } + + const auto& tileset = registry.get(tileset_id); + const auto& tileset_instance = registry.get(tileset_id); + + const auto tile_index = tile_id - tileset_instance.tile_range.first_id; + const auto tile_entity = tileset.tiles.at(static_cast(tile_index)); + + return registry.has(tile_entity); +} + auto LayerViewImpl::get_tile_encoding() const -> TileEncoding { return _get_tile_format().encoding; diff --git a/source/core/src/tactile/core/event/map_event_handler.cpp b/source/core/src/tactile/core/event/map_event_handler.cpp index 5fa882b96d..88787c6499 100644 --- a/source/core/src/tactile/core/event/map_event_handler.cpp +++ b/source/core/src/tactile/core/event/map_event_handler.cpp @@ -2,12 +2,15 @@ #include "tactile/core/event/map_event_handler.hpp" +#include // move + #include #include "tactile/base/io/save/save_format.hpp" #include "tactile/base/numeric/vec_format.hpp" #include "tactile/base/runtime.hpp" #include "tactile/core/debug/validation.hpp" +#include "tactile/core/document/map_view_impl.hpp" #include "tactile/core/event/dialog_events.hpp" #include "tactile/core/event/event_dispatcher.hpp" #include "tactile/core/event/map_events.hpp" @@ -33,7 +36,9 @@ void MapEventHandler::install(EventDispatcher& dispatcher) using Self = MapEventHandler; dispatcher.bind(this); dispatcher.bind(this); + dispatcher.bind(this); dispatcher.bind(this); + dispatcher.bind(this); } void MapEventHandler::on_show_new_map_dialog(const ShowNewMapDialogEvent&) @@ -90,6 +95,12 @@ void MapEventHandler::on_show_open_map_dialog(const ShowOpenMapDialogEvent&) document.set_format(*format_id); } +void MapEventHandler::on_show_godot_export_dialog(const ShowGodotExportDialogEvent&) +{ + TACTILE_LOG_TRACE("ShowGodotExportDialogEvent"); + mWidgetManager->get_godot_export_dialog().open(); +} + void MapEventHandler::on_create_map(const CreateMapEvent& event) { TACTILE_LOG_TRACE("CreateMapEvent(orientation: {}, size: {}, tile_size: {})", @@ -104,6 +115,42 @@ void MapEventHandler::on_create_map(const CreateMapEvent& event) } } +void MapEventHandler::on_export_as_godot_scene(const ExportAsGodotSceneEvent& event) const +{ + TACTILE_LOG_TRACE("ExportMapEvent"); + + const auto* save_format = mRuntime->get_save_format(SaveFormatId::kGodotTscn); + if (!save_format) { + TACTILE_LOG_ERROR("Godot plugin is not enabled"); + return; + } + + const auto* document = dynamic_cast(mModel->get_current_document()); + if (!document) { + TACTILE_LOG_ERROR("No current document"); + return; + } + + SaveFormatExtraSettings extra_settings {}; + extra_settings["version"] = Attribute {event.version}; + extra_settings["ellipse_polygon_vertices"] = Attribute {32}; + + const SaveFormatWriteOptions options { + .extra = std::move(extra_settings), + .base_dir = event.project_dir, + .use_external_tilesets = false, + .use_indentation = false, + .fold_tile_layer_data = false, + }; + + const MapViewImpl map_view {document}; + const auto save_result = save_format->save_map(map_view, options); + + if (!save_result.has_value()) { + TACTILE_LOG_ERROR("Could not export Godot scene: {}", save_result.error().message()); + } +} + auto MapEventHandler::_guess_save_format(const std::filesystem::path& path) -> std::optional { diff --git a/source/core/src/tactile/core/tile/tileset.cpp b/source/core/src/tactile/core/tile/tileset.cpp index 790e44b125..e9460a4b24 100644 --- a/source/core/src/tactile/core/tile/tileset.cpp +++ b/source/core/src/tactile/core/tile/tileset.cpp @@ -160,7 +160,7 @@ auto make_tileset(Registry& registry, const auto tileset_id = registry.make_entity(); registry.add(tileset_id); - auto& texture = registry.add(tileset_id, std::move(*texture_result)); + const auto& texture = registry.add(tileset_id, std::move(*texture_result)); _add_viewport_component(registry, tileset_id, texture.size); diff --git a/source/core/src/tactile/core/ui/common/attribute_widgets.cpp b/source/core/src/tactile/core/ui/common/attribute_widgets.cpp index de2984134f..d748844cf0 100644 --- a/source/core/src/tactile/core/ui/common/attribute_widgets.cpp +++ b/source/core/src/tactile/core/ui/common/attribute_widgets.cpp @@ -64,8 +64,8 @@ auto push_string_input(const char* id, const Attribute::string_type& str) return new_str; } -auto push_int_input(const char* id, - Attribute::int_type value) -> std::optional +auto push_int_input(const char* id, Attribute::int_type value) + -> std::optional { const IdScope scope {id}; std::optional new_value {}; @@ -78,8 +78,8 @@ auto push_int_input(const char* id, return new_value; } -auto push_int2_input(const char* id, - Attribute::int2_type value) -> std::optional +auto push_int2_input(const char* id, Attribute::int2_type value) + -> std::optional { const IdScope scope {id}; std::optional new_value {}; @@ -92,8 +92,8 @@ auto push_int2_input(const char* id, return new_value; } -auto push_int3_input(const char* id, - Attribute::int3_type value) -> std::optional +auto push_int3_input(const char* id, Attribute::int3_type value) + -> std::optional { const IdScope scope {id}; std::optional new_value {}; @@ -106,8 +106,8 @@ auto push_int3_input(const char* id, return new_value; } -auto push_int4_input(const char* id, - Attribute::int4_type value) -> std::optional +auto push_int4_input(const char* id, Attribute::int4_type value) + -> std::optional { const IdScope scope {id}; std::optional new_value {}; @@ -120,8 +120,8 @@ auto push_int4_input(const char* id, return new_value; } -auto push_float_input(const char* id, - Attribute::float_type value) -> std::optional +auto push_float_input(const char* id, Attribute::float_type value) + -> std::optional { const IdScope scope {id}; std::optional new_value {}; @@ -134,8 +134,8 @@ auto push_float_input(const char* id, return new_value; } -auto push_float2_input(const char* id, - Attribute::float2_type value) -> std::optional +auto push_float2_input(const char* id, Attribute::float2_type value) + -> std::optional { const IdScope scope {id}; std::optional new_value {}; @@ -148,8 +148,8 @@ auto push_float2_input(const char* id, return new_value; } -auto push_float3_input(const char* id, - Attribute::float3_type value) -> std::optional +auto push_float3_input(const char* id, Attribute::float3_type value) + -> std::optional { const IdScope scope {id}; std::optional new_value {}; @@ -162,8 +162,8 @@ auto push_float3_input(const char* id, return new_value; } -auto push_float4_input(const char* id, - Attribute::float4_type value) -> std::optional +auto push_float4_input(const char* id, Attribute::float4_type value) + -> std::optional { const IdScope scope {id}; std::optional new_value {}; @@ -188,8 +188,8 @@ auto push_bool_input(const char* id, bool value) -> std::optional return new_value; } -auto push_path_input(const char* id, - const Attribute::path_type& path) -> std::optional +auto push_path_input(const char* id, const Attribute::path_type& path) + -> std::optional { const IdScope scope {id}; std::optional new_path {}; @@ -215,6 +215,33 @@ auto push_path_input(const char* id, return new_path; } +auto push_directory_input(const char* id, const Attribute::path_type& path) + -> std::optional +{ + const IdScope scope {id}; + std::optional new_path {}; + +#if TACTILE_OS_WINDOWS + const auto path_string = path.string(); + const auto* path_c_str = path_string.c_str(); +#else + const auto* path_c_str = path.c_str(); +#endif + + if (push_icon_button(Icon::kEllipsis)) { + if (auto selected_path = FileDialog::open_folder()) { + new_path = Attribute::path_type {std::move(*selected_path)}; + } + } + + ImGui::SameLine(); + + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted(path_c_str); + + return new_path; +} + auto push_color_input(const char* id, const Attribute::color_type& color) -> std::optional { @@ -245,8 +272,8 @@ auto push_objref_input(const char* id, const Attribute::objref_type& object) } // NOLINTNEXTLINE(*-cognitive-complexity) -auto push_attribute_input(const char* id, - const Attribute& attribute) -> std::optional +auto push_attribute_input(const char* id, const Attribute& attribute) + -> std::optional { switch (attribute.get_type()) { case AttributeType::kStr: { diff --git a/source/core/src/tactile/core/ui/dialog/godot_export_dialog.cpp b/source/core/src/tactile/core/ui/dialog/godot_export_dialog.cpp new file mode 100644 index 0000000000..5da30b6877 --- /dev/null +++ b/source/core/src/tactile/core/ui/dialog/godot_export_dialog.cpp @@ -0,0 +1,92 @@ +// Copyright (C) 2024 Albin Johansson (GNU General Public License v3.0) + +#include "tactile/core/ui/dialog/godot_export_dialog.hpp" + +#include // move + +#include + +#include "tactile/core/event/event_dispatcher.hpp" +#include "tactile/core/event/map_events.hpp" +#include "tactile/core/model/model.hpp" +#include "tactile/core/platform/filesystem.hpp" +#include "tactile/core/ui/common/attribute_widgets.hpp" +#include "tactile/core/ui/common/dialogs.hpp" +#include "tactile/core/ui/common/popups.hpp" +#include "tactile/core/ui/common/style.hpp" +#include "tactile/core/ui/common/widgets.hpp" +#include "tactile/core/ui/common/window.hpp" +#include "tactile/core/ui/i18n/language.hpp" + +namespace tactile::ui { +namespace { + +void _push_godot_version_combo(int& version) +{ + constexpr const char* godot3_label = "Godot 3"; + constexpr const char* godot4_label = "Godot 4"; + const char* preview_label = version == 3 ? godot3_label : version == 4 ? godot4_label : "?"; + + if (ImGui::BeginCombo("##Version", preview_label)) { + if (ImGui::Selectable(godot3_label)) { + version = 3; + } + + if (const DisabledScope disabled {}; ImGui::Selectable(godot4_label)) { + version = 4; + } + + ImGui::EndCombo(); + } +} + +} // namespace + +void GodotExportDialog::push(const Model& model, EventDispatcher& dispatcher) +{ + const auto& language = model.get_language(); + const auto* dialog_name = language.get(StringID::kGodotExportDialog); + + center_next_window(ImGuiCond_Always); + + if (const PopupScope dialog {kModalPopup, dialog_name, ImGuiWindowFlags_AlwaysAutoResize}; + dialog.is_open()) { + const auto* version_label = language.get(StringID::kVersion); + const auto* project_dir_label = language.get(StringID::kProjectDir); + + const auto label_offset = get_alignment_offset(version_label, project_dir_label); + + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted(version_label); + ImGui::SameLine(label_offset); + _push_godot_version_combo(m_version); + + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted(project_dir_label); + ImGui::SameLine(label_offset); + if (auto project_dir = push_directory_input("##Dir", m_project_dir)) { + m_project_dir = std::move(*project_dir); + } + + const auto status = push_dialog_control_buttons(language.get(StringID::kCancel), + language.get(StringID::kSave)); + if (status == DialogStatus::kAccepted) { + dispatcher.push(m_version, std::move(m_project_dir)); + m_project_dir.clear(); + } + } + + if (m_should_open) { + ImGui::OpenPopup(dialog_name, ImGuiPopupFlags_NoReopen); + m_should_open = false; + } +} + +void GodotExportDialog::open() +{ + m_project_dir = get_user_home_directory().value_or("."); + m_version = 3; + m_should_open = true; +} + +} // namespace tactile::ui diff --git a/source/core/src/tactile/core/ui/i18n/language_parser.cpp b/source/core/src/tactile/core/ui/i18n/language_parser.cpp index a40d3bb28a..fc76befa8b 100644 --- a/source/core/src/tactile/core/ui/i18n/language_parser.cpp +++ b/source/core/src/tactile/core/ui/i18n/language_parser.cpp @@ -72,6 +72,8 @@ auto _get_noun_names() -> std::unordered_map {"dark_themes", StringID::kDarkThemes}, {"font", StringID::kFont}, {"default", StringID::kDefault}, + {"version", StringID::kVersion}, + {"project_dir", StringID::kProjectDir}, }; } @@ -189,6 +191,7 @@ auto _get_widget_names() -> std::unordered_map {"animation_dock", StringID::kAnimationDock}, {"log_dock", StringID::kLogDock}, {"style_editor", StringID::kStyleEditorWidget}, + {"godot_export_dialog", StringID::kGodotExportDialog}, }; } diff --git a/source/core/src/tactile/core/ui/menu/map_menu.cpp b/source/core/src/tactile/core/ui/menu/map_menu.cpp index ff0a8ca3db..3538abe503 100644 --- a/source/core/src/tactile/core/ui/menu/map_menu.cpp +++ b/source/core/src/tactile/core/ui/menu/map_menu.cpp @@ -81,19 +81,15 @@ void MapMenu::_push_export_as_menu(const Language& language, EventDispatcher& di if (const MenuScope export_as_menu {language.get(StringID::kExportAsMenu)}; export_as_menu.is_open()) { if (ImGui::MenuItem("Tiled JSON (TMJ)")) { - dispatcher.push(SaveFormatId::kTiledTmj); + // TODO } if (ImGui::MenuItem("Tiled XML (TMX)")) { - dispatcher.push(SaveFormatId::kTiledTmx); + // TODO } - if (ImGui::MenuItem("Godot 3 TSCN")) { - dispatcher.push(SaveFormatId::kGodotTscn); - } - - if (ImGui::MenuItem("Godot 4 TSCN")) { - // TODO dispatcher.push(SaveFormatId::kGodot4Tscn); + if (ImGui::MenuItem("Godot Scene")) { + dispatcher.push(); } } } diff --git a/source/core/src/tactile/core/ui/widget_manager.cpp b/source/core/src/tactile/core/ui/widget_manager.cpp index 07eb9e4934..afc9ce8eea 100644 --- a/source/core/src/tactile/core/ui/widget_manager.cpp +++ b/source/core/src/tactile/core/ui/widget_manager.cpp @@ -33,6 +33,7 @@ void WidgetManager::push(const Model& model, EventDispatcher& dispatcher) mNewTilesetDialog.push(model, dispatcher); mNewPropertyDialog.push(model, dispatcher); mRenamePropertyDialog.push(model, dispatcher); + m_godot_export_dialog.push(model, dispatcher); push_global_shortcuts(model, dispatcher); } @@ -47,6 +48,11 @@ auto WidgetManager::get_new_map_dialog() -> NewMapDialog& return mNewMapDialog; } +auto WidgetManager::get_godot_export_dialog() -> GodotExportDialog& +{ + return m_godot_export_dialog; +} + auto WidgetManager::get_new_tileset_dialog() -> NewTilesetDialog& { return mNewTilesetDialog; diff --git a/source/godot_tscn_format/lib/CMakeLists.txt b/source/godot_tscn_format/lib/CMakeLists.txt deleted file mode 100644 index 0a45bdaf01..0000000000 --- a/source/godot_tscn_format/lib/CMakeLists.txt +++ /dev/null @@ -1,26 +0,0 @@ -project(tactile-godot-tscn-format-lib CXX) - -add_library(tactile-godot-tscn-format SHARED) -add_library(tactile::godot_tscn_format ALIAS tactile-godot-tscn-format) - -target_sources(tactile-godot-tscn-format - PRIVATE - "src/godot_tscn_format_plugin.cpp" - - PUBLIC FILE_SET "HEADERS" BASE_DIRS "inc" FILES - "inc/tactile/godot_tscn_format/api.hpp" - "inc/tactile/godot_tscn_format/godot_tscn_format_plugin.hpp" - ) - -tactile_prepare_target(tactile-godot-tscn-format) - -target_compile_definitions(tactile-godot-tscn-format - PRIVATE - "TACTILE_BUILDING_GODOT_TSCN_FORMAT" - ) - -target_link_libraries(tactile-godot-tscn-format - PUBLIC - tactile::base - tactile::runtime - ) diff --git a/source/godot_tscn_format/lib/inc/tactile/godot_tscn_format/api.hpp b/source/godot_tscn_format/lib/inc/tactile/godot_tscn_format/api.hpp deleted file mode 100644 index b77f886996..0000000000 --- a/source/godot_tscn_format/lib/inc/tactile/godot_tscn_format/api.hpp +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (C) 2024 Albin Johansson (GNU General Public License v3.0) - -#pragma once - -#include "tactile/base/prelude.hpp" - -#ifdef TACTILE_BUILDING_GODOT_TSCN_FORMAT - #define TACTILE_TSCN_FORMAT_API TACTILE_DLL_EXPORT -#else - #define TACTILE_TSCN_FORMAT_API TACTILE_DLL_IMPORT -#endif diff --git a/source/godot_tscn_format/lib/inc/tactile/godot_tscn_format/godot_tscn_format_plugin.hpp b/source/godot_tscn_format/lib/inc/tactile/godot_tscn_format/godot_tscn_format_plugin.hpp deleted file mode 100644 index 90bbad5e5e..0000000000 --- a/source/godot_tscn_format/lib/inc/tactile/godot_tscn_format/godot_tscn_format_plugin.hpp +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (C) 2024 Albin Johansson (GNU General Public License v3.0) - -#pragma once - -#include "tactile/base/prelude.hpp" -#include "tactile/godot_tscn_format/api.hpp" -#include "tactile/runtime/plugin.hpp" - -namespace tactile { - -class TACTILE_TSCN_FORMAT_API GodotTscnFormatPlugin final : public IPlugin -{ - public: - void load(IRuntime* runtime) override; - - void unload() override; - - private: - IRuntime* mRuntime {}; -}; - -extern "C" -{ - TACTILE_TSCN_FORMAT_API auto tactile_make_plugin() -> IPlugin*; - TACTILE_TSCN_FORMAT_API void tactile_free_plugin(IPlugin* plugin); -} - -} // namespace tactile diff --git a/source/godot_tscn_format/lib/src/godot_tscn_format_plugin.cpp b/source/godot_tscn_format/lib/src/godot_tscn_format_plugin.cpp deleted file mode 100644 index e9c58fea8e..0000000000 --- a/source/godot_tscn_format/lib/src/godot_tscn_format_plugin.cpp +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (C) 2024 Albin Johansson (GNU General Public License v3.0) - -#include "tactile/godot_tscn_format/godot_tscn_format_plugin.hpp" - -#include // nothrow - -#include "tactile/runtime/logging.hpp" - -namespace tactile { - -void GodotTscnFormatPlugin::load(IRuntime* runtime) -{ - log(LogLevel::kTrace, "Loading Godot TSCN format plugin"); - mRuntime = runtime; -} - -void GodotTscnFormatPlugin::unload() -{ - log(LogLevel::kTrace, "Unloading Godot TSCN format plugin"); - mRuntime = nullptr; -} - -auto tactile_make_plugin() -> IPlugin* -{ - return new (std::nothrow) GodotTscnFormatPlugin {}; -} - -void tactile_free_plugin(IPlugin* plugin) -{ - delete plugin; -} - -} // namespace tactile diff --git a/source/godot_tscn_format/test/CMakeLists.txt b/source/godot_tscn_format/test/CMakeLists.txt deleted file mode 100644 index c8777fa1db..0000000000 --- a/source/godot_tscn_format/test/CMakeLists.txt +++ /dev/null @@ -1 +0,0 @@ -project(tactile-godot-tscn-format-test CXX) diff --git a/source/godot_tscn_format/CMakeLists.txt b/source/plugins/godot_tscn/CMakeLists.txt similarity index 68% rename from source/godot_tscn_format/CMakeLists.txt rename to source/plugins/godot_tscn/CMakeLists.txt index 47e475d3e1..db26da0997 100644 --- a/source/godot_tscn_format/CMakeLists.txt +++ b/source/plugins/godot_tscn/CMakeLists.txt @@ -1,4 +1,4 @@ -project(tactile-godot-tscn-format CXX) +project(tactile-plugins-godot-tscn CXX) add_subdirectory("lib") diff --git a/source/plugins/godot_tscn/lib/CMakeLists.txt b/source/plugins/godot_tscn/lib/CMakeLists.txt new file mode 100644 index 0000000000..3814576bef --- /dev/null +++ b/source/plugins/godot_tscn/lib/CMakeLists.txt @@ -0,0 +1,35 @@ +project(tactile-plugins-godot-tscn-lib CXX) + +add_library(tactile-godot-tscn SHARED) +add_library(tactile::godot_tscn ALIAS tactile-godot-tscn) + +target_sources(tactile-godot-tscn + PRIVATE + "src/gd3_document_converter.cpp" + "src/gd3_exporter.cpp" + "src/gd3_scene_writer.cpp" + "src/godot_scene_format.cpp" + "src/godot_scene_format_plugin.cpp" + + PUBLIC FILE_SET "HEADERS" BASE_DIRS "inc" FILES + "inc/tactile/godot_tscn/api.hpp" + "inc/tactile/godot_tscn/gd3_document_converter.hpp" + "inc/tactile/godot_tscn/gd3_exporter.hpp" + "inc/tactile/godot_tscn/gd3_scene_writer.hpp" + "inc/tactile/godot_tscn/gd3_types.hpp" + "inc/tactile/godot_tscn/godot_scene_format.hpp" + "inc/tactile/godot_tscn/godot_scene_format_plugin.hpp" + ) + +tactile_prepare_target(tactile-godot-tscn) + +target_compile_definitions(tactile-godot-tscn + PRIVATE + "TACTILE_BUILDING_GODOT_TSCN" + ) + +target_link_libraries(tactile-godot-tscn + PUBLIC + tactile::base + tactile::runtime + ) diff --git a/source/plugins/godot_tscn/lib/inc/tactile/godot_tscn/api.hpp b/source/plugins/godot_tscn/lib/inc/tactile/godot_tscn/api.hpp new file mode 100644 index 0000000000..3fe4acafc4 --- /dev/null +++ b/source/plugins/godot_tscn/lib/inc/tactile/godot_tscn/api.hpp @@ -0,0 +1,11 @@ +// Copyright (C) 2024 Albin Johansson (GNU General Public License v3.0) + +#pragma once + +#include "tactile/base/prelude.hpp" + +#ifdef TACTILE_BUILDING_GODOT_TSCN + #define TACTILE_GODOT_API TACTILE_DLL_EXPORT +#else + #define TACTILE_GODOT_API TACTILE_DLL_IMPORT +#endif diff --git a/source/plugins/godot_tscn/lib/inc/tactile/godot_tscn/gd3_document_converter.hpp b/source/plugins/godot_tscn/lib/inc/tactile/godot_tscn/gd3_document_converter.hpp new file mode 100644 index 0000000000..91c2484c02 --- /dev/null +++ b/source/plugins/godot_tscn/lib/inc/tactile/godot_tscn/gd3_document_converter.hpp @@ -0,0 +1,62 @@ +// Copyright (C) 2024 Albin Johansson (GNU General Public License v3.0) + +#pragma once + +#include // size_t + +#include "tactile/base/document/document_visitor.hpp" +#include "tactile/base/io/save/save_format.hpp" +#include "tactile/godot_tscn/api.hpp" +#include "tactile/godot_tscn/gd3_types.hpp" + +namespace tactile::godot_tscn { + +/** + * A document visitor that produces intermediate Godot 3 scene representations. + */ +class TACTILE_GODOT_API Gd3DocumentConverter final : public IDocumentVisitor +{ + public: + /** + * Creates a document converter. + * + * \param options The write options to use. + */ + explicit Gd3DocumentConverter(SaveFormatWriteOptions options); + + void set_ellipse_polygon_vertices(std::size_t count); + + [[nodiscard]] + auto visit(const IComponentView& component) -> std::expected override; + + [[nodiscard]] + auto visit(const IMapView& map) -> std::expected override; + + [[nodiscard]] + auto visit(const ILayerView& layer) -> std::expected override; + + [[nodiscard]] + auto visit(const IObjectView& object) -> std::expected override; + + [[nodiscard]] + auto visit(const ITilesetView& tileset) -> std::expected override; + + [[nodiscard]] + auto visit(const ITileView& tile) -> std::expected override; + + /** + * Returns the generated Godot 3 map. + * + * \return + * An intermediate representation of a Godot 3 map. + */ + [[nodiscard]] + auto get_map() const -> const Gd3Map&; + + private: + SaveFormatWriteOptions m_options; + Gd3Map m_map; + std::size_t m_ellipse_polygon_vertices {}; +}; + +} // namespace tactile::godot_tscn diff --git a/source/plugins/godot_tscn/lib/inc/tactile/godot_tscn/gd3_exporter.hpp b/source/plugins/godot_tscn/lib/inc/tactile/godot_tscn/gd3_exporter.hpp new file mode 100644 index 0000000000..1465514fe3 --- /dev/null +++ b/source/plugins/godot_tscn/lib/inc/tactile/godot_tscn/gd3_exporter.hpp @@ -0,0 +1,27 @@ +// Copyright (C) 2024 Albin Johansson (GNU General Public License v3.0) + +#pragma once + +#include "tactile/base/container/result.hpp" +#include "tactile/base/io/save/save_format.hpp" +#include "tactile/godot_tscn/api.hpp" + +namespace tactile::godot_tscn { + +struct Gd3Map; + +/** + * Saves a Godot 3 scene. + * + * \param map The Godot 3 map to save. + * \param options The save options to use. + * + * \return + * Nothing if successful; an error code otherwise. + */ +[[nodiscard]] +TACTILE_GODOT_API auto save_godot3_scene(const Gd3Map& map, + const SaveFormatWriteOptions& options) + -> Result; + +} // namespace tactile::godot_tscn diff --git a/source/plugins/godot_tscn/lib/inc/tactile/godot_tscn/gd3_scene_writer.hpp b/source/plugins/godot_tscn/lib/inc/tactile/godot_tscn/gd3_scene_writer.hpp new file mode 100644 index 0000000000..e644991193 --- /dev/null +++ b/source/plugins/godot_tscn/lib/inc/tactile/godot_tscn/gd3_scene_writer.hpp @@ -0,0 +1,288 @@ +// Copyright (C) 2024 Albin Johansson (GNU General Public License v3.0) + +#pragma once + +#include // invocable +#include // size_t +#include // ostream +#include // string +#include // string_view + +#include "tactile/base/meta/color.hpp" +#include "tactile/base/numeric/vec.hpp" +#include "tactile/base/prelude.hpp" +#include "tactile/godot_tscn/api.hpp" +#include "tactile/godot_tscn/gd3_types.hpp" + +namespace tactile::godot_tscn { + +/** + * Utility for emitting Godot 3 scene files. + */ +class TACTILE_GODOT_API Gd3SceneWriter final +{ + public: + /** + * Creates a scene writer. + * + * \param stream The output stream to use, whose lifetime must exceed the writer. + */ + explicit Gd3SceneWriter(std::ostream& stream); + + /** + * Outputs a line break. + * + * \return + * The writer itself. + */ + auto newline() -> Gd3SceneWriter&; + + /** + * Outputs a "gd_scene" header. + * + * \param load_steps The number of load steps. + * + * \return + * The writer itself. + */ + auto gd_scene_header(std::size_t load_steps) -> Gd3SceneWriter&; + + /** + * Outputs a "gd_resource" header. + * + * \param type The resource type. + * \param load_steps The number of load steps. + * + * \return + * The writer itself. + */ + auto gd_resource_header(std::string_view type, std::size_t load_steps) -> Gd3SceneWriter&; + + /** + * Outputs a "node" header without a parent node. + * + * \param name The node name. + * \param type The node type. + * + * \return + * The writer itself. + */ + auto node_header(std::string_view name, std::string_view type) -> Gd3SceneWriter&; + + /** + * Outputs a "node" header. + * + * \param name The node name. + * \param type The node type. + * \param parent The parent node name. + * + * \return + * The writer itself. + */ + auto node_header(std::string_view name, std::string_view type, std::string_view parent) + -> Gd3SceneWriter&; + + /** + * Outputs a "resource" header. + * + * \return + * The writer itself. + */ + auto resource_header() -> Gd3SceneWriter&; + + /** + * Outputs an "ext_resource" header. + * + * \param id The associated identifier. + * \param resource The external resource. + * + * \return + * The writer itself. + */ + auto ext_resource_header(ExtResourceId id, const Gd3ExtResource& resource) + -> Gd3SceneWriter&; + + /** + * Outputs a "sub_resource" header. + * + * \param type The resource type. + * \param id The associated identifier. + * + * \return + * The writer itself. + */ + auto sub_resource_header(std::string_view type, SubResourceId id) -> Gd3SceneWriter&; + + /** + * Outputs a plain key/value pair. + * + * \param key The variable name. + * \param value The variable value. + * + * \return + * The writer itself. + */ + auto variable(std::string_view key, std::string_view value) -> Gd3SceneWriter&; + + /** + * \copydoc variable(std::string_view, std::string_view) + */ + auto variable(const std::string_view key, const char* value) -> Gd3SceneWriter& + { + return variable(key, std::string_view {value}); + } + + /** + * \copydoc variable(std::string_view, std::string_view) + */ + auto variable(std::string_view key, bool value) -> Gd3SceneWriter&; + + /** + * Outputs a plain key/value pair with quotes around the value. + * + * \param key The variable name. + * \param value The variable value. + * + * \return + * The writer itself. + */ + auto variable_quoted(std::string_view key, std::string_view value) -> Gd3SceneWriter&; + + /** + * Outputs a "Vector2" variable. + * + * \param key The variable name. + * \param vec The variable value. + * + * \return + * The writer itself. + */ + auto vector2_variable(std::string_view key, const Int2& vec) -> Gd3SceneWriter&; + + /** + * \copydoc vector2_variable(std::string_view, const Int2&) + */ + auto vector2_variable(std::string_view key, const Float2& vec) -> Gd3SceneWriter&; + + /** + * Outputs a "Rect2" variable. + * + * \param key The variable name. + * \param rect The variable value. + * + * \return + * The writer itself. + */ + auto rect2_variable(std::string_view key, const Int4& rect) -> Gd3SceneWriter&; + + /** + * Outputs a "SubResource" variable. + * + * \param key The variable name. + * \param id The variable value. + * + * \return + * The writer itself. + */ + auto sub_resource_variable(std::string_view key, SubResourceId id) -> Gd3SceneWriter&; + + /** + * Outputs an "ExtResource" variable. + * + * \param key The variable name. + * \param id The variable value. + * + * \return + * The writer itself. + */ + auto ext_resource_variable(std::string_view key, ExtResourceId id) -> Gd3SceneWriter&; + + /** + * Outputs a "Color" variable. + * + * \param key The variable name. + * \param color The variable value. + * + * \return + * The writer itself. + */ + auto color_variable(std::string_view key, const FColor& color) -> Gd3SceneWriter&; + + /** + * Outputs a variable with elements from a given range. + * + * \tparam T The type of the range. + * \tparam Emitter The type of the custom emitter. + * + * \param key The variable name. + * \param type The variable type. + * \param range The range of elements to include. + * \param emitter The function object used to output the range elements. + * + * \return + * The writer itself. + */ + template Emitter> + auto sequence_variable(const std::string_view key, + const std::string_view type, + const T& range, + const Emitter& emitter) -> Gd3SceneWriter& + { + _emit_key_prefix(); + *m_stream << key << " = " << type << "( "; + + std::size_t index {0}; + for (const auto& elem : range) { + if (index != 0) { + *m_stream << ", "; + } + + emitter(*m_stream, elem); + + ++index; + } + + *m_stream << " )\n"; + + return *this; + } + + /** + * Outputs a variable with elements from a given range, using operator<< to output elements. + * + * \tparam T The type of the range. + * + * \param key The variable name. + * \param type The variable type. + * \param range The range of elements to include. + * + * \return + * The writer itself. + */ + template + auto sequence_variable(const std::string_view key, + const std::string_view type, + const T& range) -> Gd3SceneWriter& + { + const auto streamer = [](std::ostream& stream, const typename T::value_type& elem) { + stream << elem; + }; + + return sequence_variable(key, type, range, streamer); + } + + /** + * Sets the prefix that is prepended to variable keys. + * + * \param prefix The new key prefix. + */ + void set_key_prefix(std::string prefix); + + private: + std::ostream* m_stream; + std::string m_key_prefix; + + void _emit_key_prefix() const; +}; + +} // namespace tactile::godot_tscn diff --git a/source/plugins/godot_tscn/lib/inc/tactile/godot_tscn/gd3_types.hpp b/source/plugins/godot_tscn/lib/inc/tactile/godot_tscn/gd3_types.hpp new file mode 100644 index 0000000000..bbd2374054 --- /dev/null +++ b/source/plugins/godot_tscn/lib/inc/tactile/godot_tscn/gd3_types.hpp @@ -0,0 +1,202 @@ +// Copyright (C) 2024 Albin Johansson (GNU General Public License v3.0) + +#pragma once + +#include // size_t +#include // int32_t +#include // path +#include // string +#include // pair +#include // variant +#include // vector + +#include "tactile/base/container/string_map.hpp" +#include "tactile/base/id.hpp" +#include "tactile/base/meta/attribute.hpp" +#include "tactile/base/numeric/index_2d.hpp" +#include "tactile/base/numeric/vec.hpp" + +namespace tactile::godot_tscn { + +/** External resource identifier. */ +using ExtResourceId = std::int32_t; + +/** Subresource identifier. */ +using SubResourceId = std::int32_t; + +struct Gd3Metadata final +{ + StringMap props; + StringMap> comps; +}; + +struct Gd3ExtResource final +{ + std::string path; + std::string type; +}; + +using Gd3ExtResourceMap = std::unordered_map; + +struct Gd3AtlasTexture final +{ + ExtResourceId atlas_id; + Int4 region; +}; + +using Gd3AtlasTextureMap = std::unordered_map; + +struct Gd3Animation final +{ + std::string name; + std::vector frames; // Atlas textures + float speed; +}; + +struct Gd3SpriteFrames final +{ + SubResourceId id; + std::vector animations; +}; + +struct Gd3RectShape final +{ + Float2 extents; +}; + +using Gd3RectShapeMap = std::unordered_map; + +struct Gd3Point final +{}; + +struct Gd3Rect final +{ + SubResourceId shape_id; +}; + +struct Gd3Polygon final +{ + std::vector points; +}; + +struct Gd3Object final +{ + constexpr static std::size_t kPointTypeIndex = 0; + constexpr static std::size_t kRectTypeIndex = 1; + constexpr static std::size_t kPolygonTypeIndex = 2; + + std::string name; + std::string parent; + Float2 position; + Gd3Metadata meta; + std::variant value; + bool visible; +}; + +struct Gd3ObjectLayer final +{ + std::vector objects; +}; + +struct Gd3EncodedTile final +{ + std::int32_t position; + std::int32_t tile_atlas; + std::int32_t tile_index; +}; + +struct Gd3TileAnimation final +{ + std::string parent; + Index2D position; + TileID tile_id; +}; + +struct Gd3Layer; + +struct Gd3GroupLayer final +{ + std::vector layers; +}; + +struct Gd3TileLayer final +{ + Int2 cell_size; + std::vector tiles; + std::vector animations; +}; + +struct Gd3Layer final +{ + constexpr static std::size_t kTileLayerTypeIndex = 0; + constexpr static std::size_t kObjectLayerTypeIndex = 1; + constexpr static std::size_t kGroupLayerTypeIndex = 2; + + std::string name; + std::string parent; + std::variant value; + Gd3Metadata meta; + float opacity; + bool visible; +}; + +struct Gd3Resources final +{ + ExtResourceId next_ext_resource_id; + SubResourceId next_sub_resource_id; + Gd3ExtResourceMap ext_resources; +}; + +struct Gd3TileAtlas final +{ + std::string name; + std::filesystem::path image_path; + ExtResourceId texture_id; + TileID first_tile_id; + std::int32_t tile_count; + std::int32_t column_count; + Int2 tile_size; + Int2 image_size; +}; + +struct Gd3Tileset final +{ + SubResourceId id; + Gd3Metadata meta; + std::vector atlases; +}; + +struct Gd3Map final +{ + Gd3Metadata meta; + Gd3Resources resources; + Int2 tile_size; + Gd3Tileset tileset; + std::vector layers; + std::unordered_map tileset_texture_ids; + Gd3SpriteFrames sprite_frames; + Gd3AtlasTextureMap atlas_textures; + Gd3RectShapeMap rect_shapes; +}; + +[[nodiscard]] +constexpr auto find_tile_atlas(const Gd3Tileset& gd_tileset, const TileID tile_id) + -> std::pair +{ + std::int32_t atlas_index {0}; + + for (const auto& gd_tile_atlas : gd_tileset.atlases) { + const auto first_id = gd_tile_atlas.first_tile_id; + const auto last_id = gd_tile_atlas.first_tile_id + gd_tile_atlas.tile_count; + + if (tile_id >= first_id && tile_id < last_id) { + return {atlas_index, &gd_tile_atlas}; + } + + ++atlas_index; + } + + return {-1, nullptr}; +} + +} // namespace tactile::godot_tscn diff --git a/source/plugins/godot_tscn/lib/inc/tactile/godot_tscn/godot_scene_format.hpp b/source/plugins/godot_tscn/lib/inc/tactile/godot_tscn/godot_scene_format.hpp new file mode 100644 index 0000000000..c2135a9d8c --- /dev/null +++ b/source/plugins/godot_tscn/lib/inc/tactile/godot_tscn/godot_scene_format.hpp @@ -0,0 +1,35 @@ +// Copyright (C) 2024 Albin Johansson (GNU General Public License v3.0) + +#pragma once + +#include "tactile/base/io/save/save_format.hpp" +#include "tactile/godot_tscn/api.hpp" + +namespace tactile::godot_tscn { + +/** + * Provides support for the scene format used by the Godot game engine. + * + * \details + * This save format supports the following custom settings. + * - \c "version": An integer indicating which major version of Godot to target. + * - \c "ellipse_polygon_vertices": An integer indicating the number of vertices to use when + * approximating ellipse objects as polygons. + * + * \see https://docs.godotengine.org/en/stable/contributing/development/file_formats/tscn.html + * \see https://docs.godotengine.org/en/3.6/development/file_formats/tscn.html + */ +class TACTILE_GODOT_API GodotSceneFormat final : public ISaveFormat +{ + public: + [[nodiscard]] + auto load_map(const std::filesystem::path& map_path, + const SaveFormatReadOptions& options) const + -> std::expected override; + + [[nodiscard]] + auto save_map(const IMapView& map, const SaveFormatWriteOptions& options) const + -> std::expected override; +}; + +} // namespace tactile::godot_tscn diff --git a/source/plugins/godot_tscn/lib/inc/tactile/godot_tscn/godot_scene_format_plugin.hpp b/source/plugins/godot_tscn/lib/inc/tactile/godot_tscn/godot_scene_format_plugin.hpp new file mode 100644 index 0000000000..bab2aa2e8b --- /dev/null +++ b/source/plugins/godot_tscn/lib/inc/tactile/godot_tscn/godot_scene_format_plugin.hpp @@ -0,0 +1,32 @@ +// Copyright (C) 2024 Albin Johansson (GNU General Public License v3.0) + +#pragma once + +#include // unique_ptr + +#include "tactile/base/prelude.hpp" +#include "tactile/godot_tscn/api.hpp" +#include "tactile/godot_tscn/godot_scene_format.hpp" +#include "tactile/runtime/plugin.hpp" + +namespace tactile::godot_tscn { + +class TACTILE_GODOT_API GodotSceneFormatPlugin final : public IPlugin +{ + public: + void load(IRuntime* runtime) override; + + void unload() override; + + private: + IRuntime* m_runtime {}; + std::unique_ptr m_format {}; +}; + +extern "C" +{ + TACTILE_GODOT_API auto tactile_make_plugin() -> IPlugin*; + TACTILE_GODOT_API void tactile_free_plugin(IPlugin* plugin); +} + +} // namespace tactile::godot_tscn diff --git a/source/plugins/godot_tscn/lib/src/gd3_document_converter.cpp b/source/plugins/godot_tscn/lib/src/gd3_document_converter.cpp new file mode 100644 index 0000000000..fda79e5b28 --- /dev/null +++ b/source/plugins/godot_tscn/lib/src/gd3_document_converter.cpp @@ -0,0 +1,432 @@ +// Copyright (C) 2024 Albin Johansson (GNU General Public License v3.0) + +#include "tactile/godot_tscn/gd3_document_converter.hpp" + +#include // replace +#include // assert +#include // sin, cos +#include // path +#include // format +#include // pi_v +#include // runtime_error +#include // string +#include // move + +#include "tactile/base/document/layer_view.hpp" +#include "tactile/base/document/map_view.hpp" +#include "tactile/base/document/meta_view.hpp" +#include "tactile/base/document/object_view.hpp" +#include "tactile/base/document/tile_view.hpp" +#include "tactile/base/document/tileset_view.hpp" +#include "tactile/base/numeric/literals.hpp" +#include "tactile/base/numeric/saturate_cast.hpp" + +namespace tactile::godot_tscn { +namespace { + +using namespace std::string_literals; + +[[nodiscard]] +auto _escape_name(const std::string_view str) -> std::string +{ + std::string copy {str}; + + for (const auto bad_char : {'.', ':', '@', '/', '"', '%'}) { + std::ranges::replace(copy, bad_char, '_'); + } + + return copy; +} + +[[nodiscard]] +auto _find_layer_at_global_index(std::vector& layers, + const std::size_t target_index, + std::size_t index = 0) -> Gd3Layer* +{ + for (auto& layer : layers) { + if (index == target_index) { + return &layer; + } + + ++index; + + if (auto* group = std::get_if(&layer.value)) { + if (auto* found_layer = + _find_layer_at_global_index(group->layers, target_index, index)) { + return found_layer; + } + } + } + + return nullptr; +} + +[[nodiscard]] +auto _approximate_ellipse_as_polygon(const Float2 radius, const std::size_t point_count) + -> std::vector +{ + std::vector points {}; + points.reserve(point_count); + + const auto n = static_cast(point_count); + + for (auto index = 0_uz; index < point_count; ++index) { + const auto theta = static_cast(index) / n * std::numbers::pi_v * 2.0f; + + auto& point = points.emplace_back(); + point.set_x(radius.x() * std::cos(theta)); + point.set_y(radius.y() * std::sin(theta)); + } + + return points; +} + +[[nodiscard]] +auto _convert_meta(const IMetaView& meta) -> Gd3Metadata +{ + Gd3Metadata gd_meta {}; + + const auto prop_count = meta.property_count(); + for (auto prop_index = 0_uz; prop_index < prop_count; ++prop_index) { + const auto& [key, value] = meta.get_property(prop_index); + gd_meta.props.insert_or_assign(key, value); + } + + return gd_meta; +} + +void _convert_common_layer_data(const ILayerView& layer, + Gd3Layer& gd_layer, + std::string parent_path) +{ + gd_layer.name = _escape_name(layer.get_meta().get_name()); + gd_layer.meta = _convert_meta(layer.get_meta()); + gd_layer.parent = std::move(parent_path); + gd_layer.opacity = layer.get_opacity(); + gd_layer.visible = layer.is_visible(); +} + +[[nodiscard]] +auto _convert_tile(const ILayerView& layer, + const Gd3Tileset& gd_tileset, + const Index2D& tile_pos, + const TileID tile_id) -> Gd3EncodedTile +{ + const auto& [gd_tile_atlas_index, gd_tile_atlas] = find_tile_atlas(gd_tileset, tile_id); + if (!gd_tile_atlas) { + throw std::runtime_error {"could not find tile atlas"}; + } + + constexpr std::int32_t tile_offset = 65'536; + + Gd3EncodedTile gd_tile {}; + gd_tile.tile_index = tile_id - gd_tile_atlas->first_tile_id; + + if (gd_tile.tile_index >= gd_tile_atlas->column_count) { + const auto position_in_tileset = layer.get_tile_position_in_tileset(tile_id).value(); + gd_tile.tile_index = saturate_cast(position_in_tileset.x) + + saturate_cast(position_in_tileset.y) * tile_offset; + } + + gd_tile.position = saturate_cast(tile_pos.x) + + saturate_cast(tile_pos.y) * tile_offset; + gd_tile.tile_atlas = gd_tile_atlas_index; + + return gd_tile; +} + +[[nodiscard]] +auto _convert_tile_layer(const ILayerView& layer, + const Int2 tile_size, + const Gd3Tileset& gd_tileset, + std::string parent_path) -> Gd3Layer +{ + Gd3Layer gd_layer {}; + _convert_common_layer_data(layer, gd_layer, std::move(parent_path)); + + auto& gd_tile_layer = gd_layer.value.emplace(); + gd_tile_layer.cell_size = tile_size; + + const auto extent = layer.get_extent().value(); + for (Extent2D::value_type row = 0; row < extent.rows; ++row) { + for (Extent2D::value_type col = 0; col < extent.cols; ++col) { + const Index2D tile_pos {.x = col, .y = row}; + + const auto tile_id = layer.get_tile(tile_pos).value(); + if (tile_id == kEmptyTile) { + continue; + } + + const auto gd_encoded_tile = _convert_tile(layer, gd_tileset, tile_pos, tile_id); + gd_tile_layer.tiles.push_back(gd_encoded_tile); + + if (layer.is_tile_animated(tile_pos)) { + auto& gd_tile_animation = gd_tile_layer.animations.emplace_back(); + gd_tile_animation.position = tile_pos; + gd_tile_animation.tile_id = tile_id; + gd_tile_animation.parent = std::format("{}/{}", gd_layer.parent, gd_layer.name); + } + } + } + + return gd_layer; +} + +[[nodiscard]] +auto _prepare_object_layer(const ILayerView& layer, std::string parent_path) -> Gd3Layer +{ + Gd3Layer gd_layer {}; + _convert_common_layer_data(layer, gd_layer, std::move(parent_path)); + + auto& gd_object_layer = gd_layer.value.emplace(); + gd_object_layer.objects.reserve(layer.object_count()); + + return gd_layer; +} + +[[nodiscard]] +auto _prepare_group_layer(const ILayerView& layer, std::string parent_path) -> Gd3Layer +{ + Gd3Layer gd_layer {}; + _convert_common_layer_data(layer, gd_layer, std::move(parent_path)); + + auto& gd_group_layer = gd_layer.value.emplace(); + gd_group_layer.layers.reserve(layer.layer_count()); + + return gd_layer; +} + +void _convert_tile_animation(const ITileView& tile, + Gd3Map& gd_map, + const ExtResourceId texture_id) +{ + const auto frame_count = tile.animation_frame_count(); + assert(frame_count > 0); + + const auto& tileset = tile.get_parent_tileset(); + const auto tileset_columns = saturate_cast(tileset.column_count()); + + const auto tile_id = tileset.get_first_tile_id() + tile.get_index(); + const auto tile_size = tileset.get_tile_size(); + + Gd3Animation gd_animation {}; + gd_animation.name = std::format("Tile {}", tile_id); + gd_animation.frames.reserve(frame_count); + + const auto [_, first_frame_duration_ms] = tile.get_animation_frame(0); + gd_animation.speed = 1'000.0f / static_cast(first_frame_duration_ms.count()); + + for (auto frame_index = 0_uz; frame_index < frame_count; ++frame_index) { + const auto [frame_tile_index, frame_duration] = tile.get_animation_frame(frame_index); + const auto frame_tile_pos = Index2D::from_1d(frame_tile_index, tileset_columns); + + const Gd3AtlasTexture frame_texture { + .atlas_id = texture_id, + .region = + Int4 { + saturate_cast(frame_tile_pos.x * + saturate_cast(tile_size.x())), + saturate_cast(frame_tile_pos.y * + saturate_cast(tile_size.y())), + tile_size.x(), + tile_size.y(), + }, + }; + + const auto atlas_texture_id = gd_map.resources.next_sub_resource_id++; + gd_map.atlas_textures.insert_or_assign(atlas_texture_id, frame_texture); + + gd_animation.frames.push_back(atlas_texture_id); + } + + if (gd_map.sprite_frames.animations.empty()) { + gd_map.sprite_frames.id = gd_map.resources.next_sub_resource_id++; + } + + gd_map.sprite_frames.animations.push_back(std::move(gd_animation)); +} + +} // namespace + +Gd3DocumentConverter::Gd3DocumentConverter(SaveFormatWriteOptions options) + : m_options {std::move(options)}, + m_map {} +{} + +void Gd3DocumentConverter::set_ellipse_polygon_vertices(const std::size_t count) +{ + m_ellipse_polygon_vertices = count; +} + +auto Gd3DocumentConverter::visit(const IComponentView&) -> std::expected +{ + return {}; +} + +auto Gd3DocumentConverter::visit(const IMapView& map) -> std::expected +{ + m_map.resources.next_ext_resource_id = 1; + m_map.resources.next_sub_resource_id = 1; + + m_map.tileset.id = m_map.resources.next_sub_resource_id++; + m_map.sprite_frames.id = m_map.resources.next_sub_resource_id++; + + m_map.tile_size = map.get_tile_size(); + m_map.meta = _convert_meta(map.get_meta()); + + return {}; +} + +auto Gd3DocumentConverter::visit(const ILayerView& layer) + -> std::expected +{ + const auto* parent_layer = layer.get_parent_layer(); + + auto* gd_parent_layer = + parent_layer != nullptr + ? _find_layer_at_global_index(m_map.layers, parent_layer->get_global_index()) + : nullptr; + + const auto parent_path = + gd_parent_layer != nullptr + ? std::format("{}/{}", gd_parent_layer->parent, gd_parent_layer->name) + : "."s; + + Gd3Layer gd_layer {}; + switch (layer.get_type()) { + case LayerType::kTileLayer: { + gd_layer = _convert_tile_layer(layer, m_map.tile_size, m_map.tileset, parent_path); + break; + } + + case LayerType::kObjectLayer: { + gd_layer = _prepare_object_layer(layer, parent_path); + break; + } + + case LayerType::kGroupLayer: { + gd_layer = _prepare_group_layer(layer, parent_path); + break; + } + } + + if (gd_parent_layer != nullptr) { + std::get(gd_parent_layer->value).layers.push_back(std::move(gd_layer)); + } + else { + m_map.layers.push_back(std::move(gd_layer)); + } + + return {}; +} + +auto Gd3DocumentConverter::visit(const IObjectView& object) + -> std::expected +{ + const auto* parent_layer = object.get_parent_layer(); + if (!parent_layer) { + // TODO + return {}; + } + + auto* gd_parent_layer = + _find_layer_at_global_index(m_map.layers, parent_layer->get_global_index()); + if (!gd_parent_layer) { + return std::unexpected {std::make_error_code(std::errc::invalid_argument)}; + } + + auto object_name = std::format("Object {}", object.get_id()); + if (const auto real_object_name = object.get_meta().get_name(); !real_object_name.empty()) { + object_name += std::format(" ('{}')", _escape_name(real_object_name)); + } + + Gd3Object gd_object {}; + gd_object.name = std::move(object_name); + gd_object.parent = std::format("{}/{}", gd_parent_layer->parent, gd_parent_layer->name); + gd_object.meta = _convert_meta(object.get_meta()); + gd_object.visible = object.is_visible(); + + switch (object.get_type()) { + case ObjectType::kPoint: { + gd_object.position = object.get_position(); + gd_object.value.emplace(); + break; + } + case ObjectType::kRect: { + gd_object.position = object.get_position() + object.get_size() * 0.5f; + + const auto shape_id = m_map.resources.next_sub_resource_id++; + + auto& gd_rect = gd_object.value.emplace(); + gd_rect.shape_id = shape_id; + + m_map.rect_shapes[shape_id] = Gd3RectShape { + .extents = object.get_size() * 0.5f, + }; + + break; + } + case ObjectType::kEllipse: { + gd_object.position = object.get_position() + object.get_size() * 0.5f; + + auto& gd_polygon = gd_object.value.emplace(); + + const auto radius = object.get_size() * 0.5f; + gd_polygon.points = _approximate_ellipse_as_polygon(radius, m_ellipse_polygon_vertices); + + break; + } + } + + auto& gd_object_layer = std::get(gd_parent_layer->value); + gd_object_layer.objects.push_back(std::move(gd_object)); + + return {}; +} + +auto Gd3DocumentConverter::visit(const ITilesetView& tileset) + -> std::expected +{ + const auto& source_image_path = tileset.get_image_path(); + + const auto texture_id = m_map.resources.next_ext_resource_id++; + const auto texture_image_name = source_image_path.filename().string(); + + const Gd3ExtResource texture_resource { + .path = std::format("res://{}", texture_image_name), + .type = "Texture", + }; + + m_map.resources.ext_resources[texture_id] = texture_resource; + m_map.tileset_texture_ids[tileset.get_first_tile_id()] = texture_id; + + auto& gd_tile_atlas = m_map.tileset.atlases.emplace_back(); + gd_tile_atlas.name = _escape_name(tileset.get_meta().get_name()); + gd_tile_atlas.image_path = source_image_path; + gd_tile_atlas.texture_id = texture_id; + gd_tile_atlas.first_tile_id = tileset.get_first_tile_id(); + gd_tile_atlas.tile_count = static_cast(tileset.tile_count()); + gd_tile_atlas.column_count = static_cast(tileset.column_count()); + gd_tile_atlas.tile_size = tileset.get_tile_size(); + gd_tile_atlas.image_size = tileset.get_image_size(); + + return {}; +} + +auto Gd3DocumentConverter::visit(const ITileView& tile) -> std::expected +{ + if (tile.animation_frame_count() > 0) { + const auto& tileset = tile.get_parent_tileset(); + const auto texture_res_id = m_map.tileset_texture_ids.at(tileset.get_first_tile_id()); + _convert_tile_animation(tile, m_map, texture_res_id); + } + + return {}; +} + +auto Gd3DocumentConverter::get_map() const -> const Gd3Map& +{ + return m_map; +} + +} // namespace tactile::godot_tscn diff --git a/source/plugins/godot_tscn/lib/src/gd3_exporter.cpp b/source/plugins/godot_tscn/lib/src/gd3_exporter.cpp new file mode 100644 index 0000000000..ab6686266f --- /dev/null +++ b/source/plugins/godot_tscn/lib/src/gd3_exporter.cpp @@ -0,0 +1,512 @@ +// Copyright (C) 2024 Albin Johansson (GNU General Public License v3.0) + +#include "tactile/godot_tscn/gd3_exporter.hpp" + +#include // size_t +#include // format +#include // ofstream +#include // quoted, setprecision +#include // boolalpha, fixed +#include // ostream +#include // stringstream + +#include "tactile/base/numeric/saturate_cast.hpp" +#include "tactile/godot_tscn/gd3_scene_writer.hpp" +#include "tactile/godot_tscn/gd3_types.hpp" +#include "tactile/runtime/logging.hpp" + +namespace tactile::godot_tscn { +namespace { + +void _emit_attribute(std::ostream& stream, const std::string_view name, const Attribute& value) +{ + stream << std::quoted(name) << ": "; + + switch (value.get_type()) { + case AttributeType::kStr: { + stream << std::quoted(value.as_string()); + break; + } + case AttributeType::kInt: { + stream << value.as_int(); + break; + } + case AttributeType::kInt2: { + const auto& vec = value.as_int2(); + stream << "Vector2( " << vec.x() << ", " << vec.y() << " )"; + break; + } + case AttributeType::kInt3: { + const auto& vec = value.as_int3(); + stream << "Vector3( " << vec.x() << ", " << vec.y() << ", " << vec.z() << " )"; + break; + } + case AttributeType::kInt4: { + const auto& vec = value.as_int4(); + stream << "[ " << vec.x() << ", " << vec.y() << ", " << vec.z() << ", " << vec.w() + << " ]"; + break; + } + case AttributeType::kFloat: { + stream << value.as_float(); + break; + } + case AttributeType::kFloat2: { + const auto& vec = value.as_float2(); + stream << "Vector2( " << vec.x() << ", " << vec.y() << " )"; + break; + } + case AttributeType::kFloat3: { + const auto& vec = value.as_float3(); + stream << "Vector3( " << vec.x() << ", " << vec.y() << ", " << vec.z() << " )"; + break; + } + case AttributeType::kFloat4: { + const auto& vec = value.as_float4(); + stream << "[ " << vec.x() << ", " << vec.y() << ", " << vec.z() << ", " << vec.w() + << " ]"; + break; + } + case AttributeType::kBool: { + stream << value.as_bool(); + break; + } + case AttributeType::kPath: { + const auto& path_string = value.as_path().string(); + stream << std::quoted(path_string); + break; + } + case AttributeType::kColor: { + // TODO use normalize function + const auto& color = value.as_color(); + + const auto r = static_cast(color.red) / 255.0f; + const auto g = static_cast(color.green) / 255.0f; + const auto b = static_cast(color.blue) / 255.0f; + const auto a = static_cast(color.alpha) / 255.0f; + + stream << "Color( " << r << ", " << g << ", " << b << ", " << a << " )"; + break; + } + case AttributeType::kObject: { + stream << value.as_object().value; + break; + } + default: throw std::runtime_error {"bad attribute"}; + } +} + +void _emit_attributes(std::ostream& stream, const StringMap& attributes) +{ + std::size_t index = 0; + for (const auto& [name, value] : attributes) { + if (index != 0) { + stream << ','; + } + + _emit_attribute(stream, name, value); + ++index; + } +} + +void _emit_components(std::ostream& stream, const StringMap>& components) +{ + std::size_t component_index = 0; + for (const auto& [comp_name, comp_attributes] : components) { + if (component_index != 0) { + stream << ", "; + } + + stream << std::quoted(comp_name) << ": {"; + _emit_attributes(stream, comp_attributes); + stream << '}'; + + ++component_index; + } +} + +void _emit_metadata(Gd3SceneWriter& writer, const Gd3Metadata& meta) +{ + const auto has_props = !meta.props.empty(); + const auto has_comps = !meta.comps.empty(); + + if (!has_props && !has_comps) { + return; + } + + std::stringstream meta_stream {}; + meta_stream << std::setprecision(3) << std::fixed << std::boolalpha; + + meta_stream << '{'; + + if (has_props) { + meta_stream << "\"properties\": {"; + _emit_attributes(meta_stream, meta.props); + meta_stream << '}'; + } + + if (has_comps) { + meta_stream << "\"components\": {"; + _emit_components(meta_stream, meta.comps); + meta_stream << '}'; + } + + meta_stream << '}'; + + const auto meta_string = meta_stream.str(); + writer.variable("__meta__", meta_string); +} + +void _emit_tile_atlas(Gd3SceneWriter& writer, + const Gd3TileAtlas& tile_atlas, + const std::size_t index) +{ + writer.set_key_prefix(std::format("{}/", index)); + + writer.variable_quoted("name", tile_atlas.name) + .ext_resource_variable("texture", tile_atlas.texture_id) + .variable("tex_offset", "Vector2( 0, 0 )") + .variable("modulate", "Color( 1, 1, 1, 1 )") + .rect2_variable("region", + Int4 {0, 0, tile_atlas.image_size.x(), tile_atlas.image_size.y()}) + .variable("tile_mode", "2") + .variable("occluder_offset", "Vector2( 0, 0 )") + .variable("navigation_offset", "Vector2( 0, 0 )") + .variable("shape_offset", "Vector2( 0, 0 )") + .variable("shape_transform", "Transform2D( 1, 0, 0, 1, 0, 0 )") + .variable("shape_one_way", "false") + .variable("shape_one_way_margin", "0.0") + .variable("shapes", "[ ]") + .variable("z_index", "0"); + + writer.set_key_prefix(std::format("{}/autotile/", index)); + + writer.variable("icon_coordinate", "Vector2( 0, 0 )") + .vector2_variable("tile_size", tile_atlas.tile_size) + .variable("spacing", "0") + .variable("occluder_map", "[ ]") + .variable("navpoly_map", "[ ]") + .variable("priority_map", "[ ]") + .variable("z_index_map", "[ ]"); + + writer.set_key_prefix(""); +} + +void _emit_atlas_texture(Gd3SceneWriter& writer, + const SubResourceId id, + const Gd3AtlasTexture& texture) +{ + writer.newline() + .sub_resource_header("AtlasTexture", id) + .ext_resource_variable("atlas", texture.atlas_id) + .rect2_variable("region", texture.region); +} + +void _emit_sprite_frames(Gd3SceneWriter& writer, const Gd3SpriteFrames& sprite_frames) +{ + if (sprite_frames.animations.empty()) { + return; + } + + writer.newline().sub_resource_header("SpriteFrames", sprite_frames.id); + + std::stringstream animations_stream {}; + animations_stream << std::setprecision(3) << std::fixed; + animations_stream << '['; + + for (std::size_t anim_index = 0; const auto& animation : sprite_frames.animations) { + if (anim_index != 0) { + animations_stream << ", "; + } + animations_stream << '{'; + + animations_stream << "\"loop\": true"; + animations_stream << ", \"name\": \"" << animation.name << "\""; + animations_stream << ", \"speed\": \"" << animation.speed << "\""; + animations_stream << ", \"frames\": ["; + + for (std::size_t frame_index = 0; const auto atlas_texture_id : animation.frames) { + if (frame_index != 0) { + animations_stream << ", "; + } + animations_stream << "SubResource( " << atlas_texture_id << " )"; + ++frame_index; + } + + animations_stream << "]}"; + ++anim_index; + } + + animations_stream << ']'; + + const auto animations_string = animations_stream.str(); + writer.variable("animations", animations_string); +} + +void _emit_tile_layer_animation_nodes(Gd3SceneWriter& writer, + const Gd3TileLayer& tile_layer, + const SubResourceId sprite_frames_id) +{ + for (const auto& animation : tile_layer.animations) { + const auto name = std::format("Tile {}", animation.position); + const auto animation_name = std::format("Tile {}", animation.tile_id); + + writer.newline() + .node_header(name, "AnimatedSprite", animation.parent) + .vector2_variable("position", to_int2(animation.position) * tile_layer.cell_size) + .sub_resource_variable("frames", sprite_frames_id) + .variable("speed_scale", "1.0") + .variable_quoted("animation", animation_name) + .variable("playing", "true") + .variable("centered", "false"); + } +} + +void _emit_tile_layer(Gd3SceneWriter& writer, + const Gd3Layer& layer, + const SubResourceId tileset_id, + const SubResourceId sprite_frames_id) +{ + const auto& tile_layer = std::get(layer.value); + + writer.newline() + .node_header(layer.name, "TileMap", layer.parent) + .sub_resource_variable("tile_set", tileset_id) + .variable("visible", layer.visible) + .vector2_variable("cell_size", tile_layer.cell_size) + .color_variable("modulate", FColor {1, 1, 1, layer.opacity}) + .variable("format", "1") + .sequence_variable("tile_data", + "PoolIntArray", + tile_layer.tiles, + [](std::ostream& stream, const Gd3EncodedTile& tile) { + stream << tile.position << ", " << tile.tile_atlas << ", " + << tile.tile_index; + }); + + _emit_metadata(writer, layer.meta); + + _emit_tile_layer_animation_nodes(writer, tile_layer, sprite_frames_id); +} + +void _emit_rect_object(Gd3SceneWriter& writer, const Gd3Object& object) +{ + const auto& rect = std::get(object.value); + + writer.newline() + .node_header(object.name, "Area2D", object.parent) + .vector2_variable("position", object.position) + .variable("visible", object.visible); + + _emit_metadata(writer, object.meta); + + const auto shape_parent_path = std::format("{}/{}", object.parent, object.name); + writer.newline() + .node_header("Shape", "CollisionShape2D", shape_parent_path) + .sub_resource_variable("shape", rect.shape_id); +} + +void _emit_polygon_object(Gd3SceneWriter& writer, const Gd3Object& object) +{ + const auto& polygon = std::get(object.value); + + writer.newline() + .node_header(object.name, "Area2D", object.parent) + .vector2_variable("position", object.position) + .variable("visible", object.visible); + + _emit_metadata(writer, object.meta); + + const auto shape_parent = std::format("{}/{}", object.parent, object.name); + + writer.newline() + .node_header("Shape", "CollisionPolygon2D", shape_parent) + .sequence_variable("polygon", + "PoolVector2Array", + polygon.points, + [](std::ostream& stream, const Float2& point) { + stream << point.x() << ", " << point.y(); + }); +} + +void _emit_point_object(Gd3SceneWriter& writer, const Gd3Object& object) +{ + writer.newline() + .node_header(object.name, "Node2D", object.parent) + .vector2_variable("position", object.position) + .variable("visible", object.visible); + + _emit_metadata(writer, object.meta); +} + +void _emit_object(Gd3SceneWriter& writer, const Gd3Object& object) +{ + switch (object.value.index()) { + case Gd3Object::kRectTypeIndex: _emit_rect_object(writer, object); break; + case Gd3Object::kPolygonTypeIndex: _emit_polygon_object(writer, object); break; + case Gd3Object::kPointTypeIndex: _emit_point_object(writer, object); break; + default: throw std::runtime_error {"bad object"}; + } +} + +void _emit_object_layer(Gd3SceneWriter& writer, const Gd3Layer& layer) +{ + const auto& object_layer = std::get(layer.value); + + writer.newline() + .node_header(layer.name, "Node2D", layer.parent) + .color_variable("modulate", FColor {1, 1, 1, layer.opacity}) + .variable("visible", layer.visible); + + _emit_metadata(writer, layer.meta); + + for (const auto& object : object_layer.objects) { + _emit_object(writer, object); + } +} + +void _emit_layer(Gd3SceneWriter& writer, + const Gd3Layer& layer, + SubResourceId tileset_id, + SubResourceId sprite_frames_id); + +void _emit_group_layer(Gd3SceneWriter& writer, + const Gd3Layer& layer, + const SubResourceId tileset_id, + const SubResourceId sprite_frames_id) +{ + const auto& group_layer = std::get(layer.value); + + writer.newline() + .node_header(layer.name, "Node2D", layer.parent) // + .color_variable("modulate", FColor {1, 1, 1, layer.opacity}) + .variable("visible", layer.visible); + + _emit_metadata(writer, layer.meta); + + for (const auto& sublayer : group_layer.layers) { + _emit_layer(writer, sublayer, tileset_id, sprite_frames_id); + } +} + +void _emit_layer(Gd3SceneWriter& writer, + const Gd3Layer& layer, + const SubResourceId tileset_id, + const SubResourceId sprite_frames_id) +{ + switch (layer.value.index()) { + case Gd3Layer::kTileLayerTypeIndex: { + _emit_tile_layer(writer, layer, tileset_id, sprite_frames_id); + break; + } + case Gd3Layer::kObjectLayerTypeIndex: { + _emit_object_layer(writer, layer); + break; + } + case Gd3Layer::kGroupLayerTypeIndex: { + _emit_group_layer(writer, layer, tileset_id, sprite_frames_id); + break; + } + default: throw std::runtime_error {"bad layer"}; + } +} + +void _emit_tileset(Gd3SceneWriter& writer, const Gd3Tileset& tileset) +{ + writer.newline().sub_resource_header("TileSet", tileset.id); + + _emit_metadata(writer, tileset.meta); + + std::size_t tileset_index = 0; + for (const auto& tile_atlas : tileset.atlases) { + _emit_tile_atlas(writer, tile_atlas, tileset_index); + ++tileset_index; + } +} + +void _emit_resources(Gd3SceneWriter& writer, const Gd3Resources& resources) +{ + for (const auto& [id, resource] : resources.ext_resources) { + writer.newline().ext_resource_header(id, resource); + } +} + +[[nodiscard]] +auto _emit_map_file(const Gd3Map& map, const SaveFormatWriteOptions& options) -> Result +{ + const auto path = options.base_dir / "map.tscn"; + log(LogLevel::kDebug, "Generating map scene '{}'", path.string()); + + std::ofstream stream {path, std::ios::out | std::ios::trunc}; + if (!stream.good()) { + return std::unexpected {ErrorCode::kBadFileStream}; + } + + Gd3SceneWriter writer {stream}; + + const auto load_steps = map.resources.ext_resources.size() + + saturate_cast(map.resources.next_sub_resource_id - 1); + writer.gd_scene_header(load_steps); + + _emit_resources(writer, map.resources); + + for (const auto& [id, texture] : map.atlas_textures) { + _emit_atlas_texture(writer, id, texture); + } + + for (const auto& [id, shape] : map.rect_shapes) { + writer.newline() + .sub_resource_header("RectangleShape2D", id) + .vector2_variable("extents", shape.extents); + } + + _emit_sprite_frames(writer, map.sprite_frames); + _emit_tileset(writer, map.tileset); + + writer.newline().node_header("Root", "Node2D"); + _emit_metadata(writer, map.meta); + + for (const auto& layer : map.layers) { + _emit_layer(writer, layer, map.tileset.id, map.sprite_frames.id); + } + + return kOK; +} + +[[nodiscard]] +auto _save_tileset_images(const Gd3Tileset& tileset, const SaveFormatWriteOptions& options) + -> Result +{ + for (const auto& tile_atlas : tileset.atlases) { + const auto dest = options.base_dir / tile_atlas.image_path.filename(); // FIXME + + log(LogLevel::kDebug, + "Copying texture '{}' to '{}'", + tile_atlas.image_path.filename().string(), + dest.string()); + + std::error_code copy_error {}; + std::filesystem::copy(tile_atlas.image_path, + dest, + std::filesystem::copy_options::overwrite_existing, + copy_error); + + if (copy_error) { + return std::unexpected {ErrorCode::kBadFileCopy}; + } + } + + return kOK; +} + +} // namespace + +auto save_godot3_scene(const Gd3Map& map, const SaveFormatWriteOptions& options) + -> Result +{ + return _save_tileset_images(map.tileset, options).and_then([&] { + return _emit_map_file(map, options); + }); +} + +} // namespace tactile::godot_tscn diff --git a/source/plugins/godot_tscn/lib/src/gd3_scene_writer.cpp b/source/plugins/godot_tscn/lib/src/gd3_scene_writer.cpp new file mode 100644 index 0000000000..1acf02d84d --- /dev/null +++ b/source/plugins/godot_tscn/lib/src/gd3_scene_writer.cpp @@ -0,0 +1,165 @@ +// Copyright (C) 2024 Albin Johansson (GNU General Public License v3.0) + +#include "tactile/godot_tscn/gd3_scene_writer.hpp" + +#include // quoted, setprecision +#include // boolalpha, fixed +#include // move + +namespace tactile::godot_tscn { + +Gd3SceneWriter::Gd3SceneWriter(std::ostream& stream) + : m_stream {&stream}, + m_key_prefix {} +{ + *m_stream << std::setprecision(3) << std::fixed; +} + +auto Gd3SceneWriter::newline() -> Gd3SceneWriter& +{ + *m_stream << '\n'; + return *this; +} + +auto Gd3SceneWriter::gd_scene_header(const std::size_t load_steps) -> Gd3SceneWriter& +{ + *m_stream << "[gd_scene load_steps=" << load_steps << " format=2]\n"; + return *this; +} + +auto Gd3SceneWriter::gd_resource_header(const std::string_view type, + const std::size_t load_steps) -> Gd3SceneWriter& +{ + *m_stream << "[gd_resource type=" << std::quoted(type) << " load_steps=" << load_steps + << " format=2]\n"; + return *this; +} + +auto Gd3SceneWriter::node_header(const std::string_view name, const std::string_view type) + -> Gd3SceneWriter& +{ + *m_stream << "[node name=" << std::quoted(name) << " type=" << std::quoted(type) << "]\n"; + return *this; +} + +auto Gd3SceneWriter::node_header(const std::string_view name, + const std::string_view type, + const std::string_view parent) -> Gd3SceneWriter& +{ + *m_stream << "[node name=" << std::quoted(name) // + << " type=" << std::quoted(type) // + << " parent=" << std::quoted(parent) << "]\n"; + return *this; +} + +auto Gd3SceneWriter::resource_header() -> Gd3SceneWriter& +{ + *m_stream << "[resource]\n"; + return *this; +} + +auto Gd3SceneWriter::ext_resource_header(const ExtResourceId id, + const Gd3ExtResource& resource) -> Gd3SceneWriter& +{ + *m_stream << "[ext_resource path=" << std::quoted(resource.path) // + << " type=" << std::quoted(resource.type) // + << " id=" << id << "]\n"; + return *this; +} + +auto Gd3SceneWriter::sub_resource_header(const std::string_view type, const SubResourceId id) + -> Gd3SceneWriter& +{ + *m_stream << "[sub_resource type=" << std::quoted(type) << " id=" << id << "]\n"; + return *this; +} + +auto Gd3SceneWriter::variable(const std::string_view key, const std::string_view value) + -> Gd3SceneWriter& +{ + _emit_key_prefix(); + *m_stream << key << " = " << value << '\n'; + return *this; +} + +auto Gd3SceneWriter::variable(const std::string_view key, const bool value) -> Gd3SceneWriter& +{ + _emit_key_prefix(); + *m_stream << key << " = " << std::boolalpha << value << '\n'; + return *this; +} + +auto Gd3SceneWriter::variable_quoted(const std::string_view key, const std::string_view value) + -> Gd3SceneWriter& +{ + _emit_key_prefix(); + *m_stream << key << " = " << std::quoted(value) << '\n'; + return *this; +} + +auto Gd3SceneWriter::vector2_variable(const std::string_view key, const Int2& vec) + -> Gd3SceneWriter& +{ + _emit_key_prefix(); + *m_stream << key << " = Vector2( " << vec.x() << ", " << vec.y() << " )\n"; + return *this; +} + +auto Gd3SceneWriter::vector2_variable(const std::string_view key, const Float2& vec) + -> Gd3SceneWriter& +{ + _emit_key_prefix(); + *m_stream << key << " = Vector2( " << vec.x() << ", " << vec.y() << " )\n"; + return *this; +} + +auto Gd3SceneWriter::rect2_variable(const std::string_view key, const Int4& rect) + -> Gd3SceneWriter& +{ + _emit_key_prefix(); + *m_stream << key << " = Rect2( " // + << rect.x() << ", " // + << rect.y() << ", " // + << rect.z() << ", " // + << rect.w() << " )\n"; + return *this; +} + +auto Gd3SceneWriter::sub_resource_variable(const std::string_view key, const SubResourceId id) + -> Gd3SceneWriter& +{ + _emit_key_prefix(); + *m_stream << key << " = SubResource( " << id << " )\n"; + return *this; +} + +auto Gd3SceneWriter::ext_resource_variable(const std::string_view key, const ExtResourceId id) + -> Gd3SceneWriter& +{ + _emit_key_prefix(); + *m_stream << key << " = ExtResource( " << id << " )\n"; + return *this; +} + +auto Gd3SceneWriter::color_variable(const std::string_view key, const FColor& color) + -> Gd3SceneWriter& +{ + _emit_key_prefix(); + *m_stream << key << " = Color( " << color.red << ", " << color.green << ", " << color.blue + << ", " << color.alpha << " )\n"; + return *this; +} + +void Gd3SceneWriter::set_key_prefix(std::string prefix) +{ + m_key_prefix = std::move(prefix); +} + +void Gd3SceneWriter::_emit_key_prefix() const +{ + if (!m_key_prefix.empty()) { + *m_stream << m_key_prefix; + } +} + +} // namespace tactile::godot_tscn diff --git a/source/plugins/godot_tscn/lib/src/godot_scene_format.cpp b/source/plugins/godot_tscn/lib/src/godot_scene_format.cpp new file mode 100644 index 0000000000..4d145349fa --- /dev/null +++ b/source/plugins/godot_tscn/lib/src/godot_scene_format.cpp @@ -0,0 +1,86 @@ +// Copyright (C) 2024 Albin Johansson (GNU General Public License v3.0) + +#include "tactile/godot_tscn/godot_scene_format.hpp" + +#include // exception +#include // make_error_code, errc + +#include "tactile/base/document/map_view.hpp" +#include "tactile/base/io/int_parser.hpp" +#include "tactile/base/numeric/saturate_cast.hpp" +#include "tactile/godot_tscn/gd3_document_converter.hpp" +#include "tactile/godot_tscn/gd3_exporter.hpp" +#include "tactile/runtime/logging.hpp" + +namespace tactile::godot_tscn { +namespace { + +[[nodiscard]] +auto _deduce_godot_version(const SaveFormatExtraSettings& settings) -> int +{ + int version {3}; + + if (const auto iter = settings.find("version"); iter != settings.end()) { + const auto parsed_version = iter->second.as_int(); + if (parsed_version == 3 || parsed_version == 4) { + version = parsed_version; + } + } + + return version; +} + +[[nodiscard]] +auto _deduce_ellipse_polygon_vertices(const SaveFormatExtraSettings& settings) -> std::size_t +{ + std::size_t vertices {32}; + + if (const auto iter = settings.find("ellipse_polygon_vertices"); iter != settings.end()) { + const auto parsed_vertices = saturate_cast(iter->second.as_int()); + if (parsed_vertices >= 4) { + vertices = parsed_vertices; + } + } + + return vertices; +} + +} // namespace + +auto GodotSceneFormat::load_map(const std::filesystem::path&, + const SaveFormatReadOptions&) const + -> std::expected +{ + return std::unexpected {std::make_error_code(std::errc::not_supported)}; +} + +auto GodotSceneFormat::save_map(const IMapView& map, + const SaveFormatWriteOptions& options) const + -> std::expected +{ + try { + const auto version = _deduce_godot_version(options.extra); + const auto ellipse_polygon_vertices = _deduce_ellipse_polygon_vertices(options.extra); + + if (version == 3) { + Gd3DocumentConverter converter {options}; + converter.set_ellipse_polygon_vertices(ellipse_polygon_vertices); + + map.accept(converter); + + const auto& gd_map = converter.get_map(); + return save_godot3_scene(gd_map, options).transform_error([](const ErrorCode) { + return std::make_error_code(std::errc::io_error); + }); + } + + return std::unexpected {std::make_error_code(std::errc::not_supported)}; + } + catch (const std::exception& error) { + log(LogLevel::kError, "Unexpected Godot export error: {}", error.what()); + } + + return std::unexpected {std::make_error_code(std::errc::io_error)}; +} + +} // namespace tactile::godot_tscn diff --git a/source/plugins/godot_tscn/lib/src/godot_scene_format_plugin.cpp b/source/plugins/godot_tscn/lib/src/godot_scene_format_plugin.cpp new file mode 100644 index 0000000000..73ca45553d --- /dev/null +++ b/source/plugins/godot_tscn/lib/src/godot_scene_format_plugin.cpp @@ -0,0 +1,41 @@ +// Copyright (C) 2024 Albin Johansson (GNU General Public License v3.0) + +#include "tactile/godot_tscn/godot_scene_format_plugin.hpp" + +#include // nothrow + +#include "tactile/base/runtime.hpp" +#include "tactile/runtime/logging.hpp" + +namespace tactile::godot_tscn { + +void GodotSceneFormatPlugin::load(IRuntime* runtime) +{ + log(LogLevel::kTrace, "Loading Godot TSCN format plugin"); + m_runtime = runtime; + + m_format = std::make_unique(); + m_runtime->set_save_format(SaveFormatId::kGodotTscn, m_format.get()); +} + +void GodotSceneFormatPlugin::unload() +{ + log(LogLevel::kTrace, "Unloading Godot TSCN format plugin"); + + m_runtime->set_save_format(SaveFormatId::kGodotTscn, nullptr); + m_format.reset(); + + m_runtime = nullptr; +} + +auto tactile_make_plugin() -> IPlugin* +{ + return new (std::nothrow) GodotSceneFormatPlugin {}; +} + +void tactile_free_plugin(IPlugin* plugin) +{ + delete plugin; +} + +} // namespace tactile::godot_tscn diff --git a/source/godot_tscn_format/test/.clang-tidy b/source/plugins/godot_tscn/test/.clang-tidy similarity index 100% rename from source/godot_tscn_format/test/.clang-tidy rename to source/plugins/godot_tscn/test/.clang-tidy diff --git a/source/plugins/godot_tscn/test/CMakeLists.txt b/source/plugins/godot_tscn/test/CMakeLists.txt new file mode 100644 index 0000000000..d49c0774b3 --- /dev/null +++ b/source/plugins/godot_tscn/test/CMakeLists.txt @@ -0,0 +1 @@ +project(tactile-plugins-godot-tscn-test CXX) diff --git a/source/runtime/lib/CMakeLists.txt b/source/runtime/lib/CMakeLists.txt index 8a598a374d..984e9a5919 100644 --- a/source/runtime/lib/CMakeLists.txt +++ b/source/runtime/lib/CMakeLists.txt @@ -13,10 +13,12 @@ target_sources(tactile-runtime "src/tactile/runtime/runtime.cpp" "src/tactile/runtime/sdl_context.cpp" "src/tactile/runtime/window.cpp" + "src/tactile/document_factory.cpp" PUBLIC FILE_SET "HEADERS" BASE_DIRS "inc" FILES "inc/tactile/runtime/api.hpp" "inc/tactile/runtime/command_line_options.hpp" + "inc/tactile/runtime/document_factory.hpp" "inc/tactile/runtime/launcher.hpp" "inc/tactile/runtime/logging.hpp" "inc/tactile/runtime/plugin.hpp" diff --git a/source/runtime/lib/inc/tactile/runtime/document_factory.hpp b/source/runtime/lib/inc/tactile/runtime/document_factory.hpp new file mode 100644 index 0000000000..4109c1135b --- /dev/null +++ b/source/runtime/lib/inc/tactile/runtime/document_factory.hpp @@ -0,0 +1,44 @@ +// Copyright (C) 2024 Albin Johansson (GNU General Public License v3.0) + +#pragma once + +#include // unique_ptr + +#include "tactile/base/document/document.hpp" +#include "tactile/base/document/map_view.hpp" +#include "tactile/base/io/save/ir.hpp" +#include "tactile/base/render/renderer.hpp" +#include "tactile/runtime/api.hpp" + +namespace tactile::runtime { + +/** + * Creates a map document from an intermediate representation. + * + * \note This function is intended for testing use only. + * + * \param renderer The associated renderer. + * \param ir_map The intermediate map to use as a reference. + * + * \return + * A unique pointer to the created map document, may be null. + */ +[[nodiscard]] +TACTILE_RUNTIME_API auto make_map_document(IRenderer& renderer, const ir::Map& ir_map) + -> std::unique_ptr; + +/** + * Creates a map view from a map document. + * + * \note This function is intended for testing use only. + * + * \param document The map document to view. + * + * \return + * A view of the given map document. A null pointer is returned if the document doesn't + * represent a map. + */ +[[nodiscard]] +TACTILE_RUNTIME_API auto make_map_view(const IDocument& document) -> std::unique_ptr; + +} // namespace tactile::runtime diff --git a/source/runtime/lib/src/tactile/document_factory.cpp b/source/runtime/lib/src/tactile/document_factory.cpp new file mode 100644 index 0000000000..2c8abfa4eb --- /dev/null +++ b/source/runtime/lib/src/tactile/document_factory.cpp @@ -0,0 +1,31 @@ +// Copyright (C) 2024 Albin Johansson (GNU General Public License v3.0) + +#include "tactile/runtime/document_factory.hpp" + +#include // move + +#include "tactile/core/document/map_document.hpp" +#include "tactile/core/document/map_view_impl.hpp" + +namespace tactile::runtime { + +auto make_map_document(IRenderer& renderer, const ir::Map& ir_map) + -> std::unique_ptr +{ + if (auto document = MapDocument::make(renderer, ir_map)) { + return std::make_unique(std::move(*document)); + } + + return nullptr; +} + +auto make_map_view(const IDocument& document) -> std::unique_ptr +{ + if (const auto* map_document = dynamic_cast(&document)) { + return std::make_unique(map_document); + } + + return nullptr; +} + +} // namespace tactile::runtime diff --git a/source/test_util/inc/tactile/test_util/document_view_mocks.hpp b/source/test_util/inc/tactile/test_util/document_view_mocks.hpp index f30946402c..6ef8e071ea 100644 --- a/source/test_util/inc/tactile/test_util/document_view_mocks.hpp +++ b/source/test_util/inc/tactile/test_util/document_view_mocks.hpp @@ -195,6 +195,13 @@ class LayerViewMock : public ILayerView MOCK_METHOD(std::optional, get_tile, (const Index2D&), (const, override)); + MOCK_METHOD(std::optional, + get_tile_position_in_tileset, + (TileID), + (const, override)); + + MOCK_METHOD(bool, is_tile_animated, (const Index2D&), (const, override)); + MOCK_METHOD(TileEncoding, get_tile_encoding, (), (const, override)); MOCK_METHOD(std::optional, get_tile_compression, (), (const, override));