From 6fa9b80d576663268778f9d76afbde88d6b044ed Mon Sep 17 00:00:00 2001 From: Casey W Date: Fri, 20 Feb 2026 12:24:46 -0500 Subject: [PATCH 1/3] Add .tide support to tbin plugin (#4308) * Update license for tbin plugin from my old name to my new one * Add .tide support to tbin plugin. * Update docs * Add blend mode support for Tiled::Map -> tbin::Map --- NEWS.md | 1 + docs/manual/export-tbin.rst | 23 +- src/plugins/tbin/tbin.qbs | 6 +- src/plugins/tbin/tbin/FakeSfml.hpp | 18 - src/plugins/tbin/tbin/Layer.hpp | 8 +- src/plugins/tbin/tbin/Map.cpp | 90 ++-- src/plugins/tbin/tbin/Map.hpp | 2 +- src/plugins/tbin/tbin/PropertyValue.hpp | 7 +- src/plugins/tbin/tbin/Tile.hpp | 21 +- src/plugins/tbin/tbin/TileSheet.hpp | 12 +- src/plugins/tbin/tbin/Vector2i.hpp | 18 + src/plugins/tbin/tbin_global.h | 2 +- src/plugins/tbin/tbinmapformat.cpp | 117 +++++ src/plugins/tbin/tbinmapformat.h | 56 +++ src/plugins/tbin/tbinplugin.cpp | 618 +++++++++++++----------- src/plugins/tbin/tbinplugin.h | 40 +- src/plugins/tbin/tidemapformat.cpp | 556 +++++++++++++++++++++ src/plugins/tbin/tidemapformat.h | 56 +++ 18 files changed, 1238 insertions(+), 413 deletions(-) delete mode 100644 src/plugins/tbin/tbin/FakeSfml.hpp create mode 100644 src/plugins/tbin/tbin/Vector2i.hpp create mode 100644 src/plugins/tbin/tbinmapformat.cpp create mode 100644 src/plugins/tbin/tbinmapformat.h create mode 100644 src/plugins/tbin/tidemapformat.cpp create mode 100644 src/plugins/tbin/tidemapformat.h diff --git a/NEWS.md b/NEWS.md index 7b9f2bc72a..87b0c59a28 100644 --- a/NEWS.md +++ b/NEWS.md @@ -27,6 +27,7 @@ * Scripting: Added TileMap.chunkSize and TileMap.compressionLevel properties * AutoMapping: Don't match rules based on empty input indexes * AutoMapping: Optimized reloading of rule maps and load rule maps on-demand +* tBIN plugin: Added support for the tIDE XML format (by Casey Warrington, #4308) * Windows: Fixed issue with opening Tile Animation Editor (#4223) * macOS: Fixed crash when JS code is JIT-compiled (#4218) * Workaround tileset view layout regression in Qt 6.9 diff --git a/docs/manual/export-tbin.rst b/docs/manual/export-tbin.rst index fb3d1546ad..89c8bd19a0 100644 --- a/docs/manual/export-tbin.rst +++ b/docs/manual/export-tbin.rst @@ -1,25 +1,32 @@ .. raw:: html -
Since Tiled 1.1
+
tBIN since Tiled 1.1
+
tIDE new in Tiled 1.12
-tBIN ----- +tBIN and tIDE +------------- -The tBIN map format is a binary format used by the `tIDE Tile Map Editor`_. +The tBIN map format is a binary format used by the `tIDE Tile Map Editor`_, +while the tIDE map format is an XML-based format used by it as well. tIDE was used by `Stardew Valley`_, a successful game that spawned many `community mods `__. Tiled ships with a plugin that enables direct editing of Stardew Valley -maps (and any other maps using the tBIN format). This plugin needs to be -enabled in *Edit > Preferences > Plugins*. It is not enabled by default +maps (and any other maps using the tBIN or tIDE formats). This plugin needs to +be enabled in *Edit > Preferences > Plugins*. It is not enabled by default because it won't store everything (most notably it doesn't support object layers in general, nor external tilesets), so you need to know what you are doing. .. note:: - The tBIN format supports setting custom properties on the tiles of a - tile layer. Since Tiled does not support this directly, "TileData" + There is only one plugin, called `tbin`. Despite its name, it enables + support for both tBIN and tIDE maps. + +.. note:: + + The tBIN and tIDE formats support setting custom properties on the tiles of + a tile layer. Since Tiled does not support this directly, "TileData" objects are created that match the location of the tile, on which such properties are then stored. diff --git a/src/plugins/tbin/tbin.qbs b/src/plugins/tbin/tbin.qbs index 41a3a42b77..38d7732f71 100644 --- a/src/plugins/tbin/tbin.qbs +++ b/src/plugins/tbin/tbin.qbs @@ -5,6 +5,10 @@ TiledPlugin { "tbin_global.h", "tbinplugin.cpp", "tbinplugin.h", + "tbinmapformat.cpp", + "tbinmapformat.h", + "tidemapformat.cpp", + "tidemapformat.h", "plugin.json", "tbin/Layer.hpp", "tbin/Map.cpp", @@ -12,6 +16,6 @@ TiledPlugin { "tbin/PropertyValue.hpp", "tbin/Tile.hpp", "tbin/TileSheet.hpp", - "tbin/FakeSfml.hpp", + "tbin/Vector2i.hpp", ] } diff --git a/src/plugins/tbin/tbin/FakeSfml.hpp b/src/plugins/tbin/tbin/FakeSfml.hpp deleted file mode 100644 index 542b41b0cd..0000000000 --- a/src/plugins/tbin/tbin/FakeSfml.hpp +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef FAKESFML_HPP -#define FAKESFML_HPP - -namespace sf -{ - using Int32 = int; - using Uint8 = unsigned char; - struct Vector2i - { - Int32 x; - Int32 y; - - Vector2i() : x(0),y(0) {} - Vector2i( Int32 x, Int32 y ) : x(x), y(y) {} - }; -} - -#endif // FAKESFML_HPP diff --git a/src/plugins/tbin/tbin/Layer.hpp b/src/plugins/tbin/tbin/Layer.hpp index fc5945e7f8..b6509bd3b3 100644 --- a/src/plugins/tbin/tbin/Layer.hpp +++ b/src/plugins/tbin/tbin/Layer.hpp @@ -1,6 +1,6 @@ /* * TBIN - * Copyright 2017, Chase Warrington + * Copyright 2017, Casey Warrington * * MIT License * @@ -30,7 +30,7 @@ #include #include -#include "FakeSfml.hpp" +#include "Vector2i.hpp" #include "PropertyValue.hpp" #include "Tile.hpp" @@ -42,8 +42,8 @@ namespace tbin std::string id; bool visible; std::string desc; - sf::Vector2i layerSize; - sf::Vector2i tileSize; + tbin::Vector2i layerSize; + tbin::Vector2i tileSize; Properties props; std::vector< Tile > tiles; }; diff --git a/src/plugins/tbin/tbin/Map.cpp b/src/plugins/tbin/tbin/Map.cpp index b293c61422..171f466791 100644 --- a/src/plugins/tbin/tbin/Map.cpp +++ b/src/plugins/tbin/tbin/Map.cpp @@ -1,6 +1,6 @@ /* * TBIN - * Copyright 2017, Chase Warrington + * Copyright 2017, Casey Warrington * * MIT License * @@ -29,7 +29,7 @@ #include #include -#include "FakeSfml.hpp" +#include "Vector2i.hpp" namespace tbin { @@ -42,17 +42,17 @@ namespace tbin } template<> - sf::Vector2i read< sf::Vector2i >( std::istream& in ) + tbin::Vector2i read< tbin::Vector2i >( std::istream& in ) { - sf::Int32 x = read< sf::Int32 >( in ); - sf::Int32 y = read< sf::Int32 >( in ); - return sf::Vector2i( x, y ); + int32_t x = read< int32_t >( in ); + int32_t y = read< int32_t >( in ); + return tbin::Vector2i( x, y ); } template<> std::string read< std::string >( std::istream& in ) { - auto len = read< sf::Int32 >( in ); + auto len = read< int32_t >( in ); std::string str( len, 0 ); in.read( &str[ 0 ], len ); return str; @@ -65,16 +65,16 @@ namespace tbin } template<> - void write< sf::Vector2i >( std::ostream& out, const sf::Vector2i& vec ) + void write< tbin::Vector2i >( std::ostream& out, const tbin::Vector2i& vec ) { - write< sf::Int32 >( out, vec.x ); - write< sf::Int32 >( out, vec.y ); + write< int32_t >( out, vec.x ); + write< int32_t >( out, vec.y ); } template<> void write< std::string >( std::ostream& out, const std::string& str ) { - write< sf::Int32 >( out, str.length() ); + write< int32_t >( out, str.length() ); out.write( &str[ 0 ], str.length() ); } @@ -82,18 +82,18 @@ namespace tbin { Properties ret; - int count = read< sf::Int32 >( in ); + int count = read< int32_t >( in ); for ( int i = 0; i < count; ++i ) { std::string key; PropertyValue value; key = read< std::string >( in ); - value.type = static_cast< PropertyValue::Type >( read< sf::Uint8 >( in ) ); + value.type = static_cast< PropertyValue::Type >( read< uint8_t >( in ) ); switch ( value.type ) { - case PropertyValue::Bool: value.data.b = read< sf::Uint8 >( in ) > 0; break; - case PropertyValue::Integer: value.data.i = read< sf::Int32 >( in ); break; + case PropertyValue::Bool: value.data.b = read< uint8_t >( in ) > 0; break; + case PropertyValue::Integer: value.data.i = read< int32_t >( in ); break; case PropertyValue::Float: value.data.f = read< float >( in ); break; case PropertyValue::String: value.dataStr = read< std::string >( in ); break; default: throw std::invalid_argument( QT_TRANSLATE_NOOP("TbinMapFormat", "Bad property type") ); @@ -108,14 +108,14 @@ namespace tbin void writeProperties( std::ostream& out, const Properties& props ) { - write< sf::Int32 >( out, props.size() ); + write< int32_t >( out, props.size() ); for ( const auto& prop : props ) { write( out, prop.first ); - write< sf::Uint8 >( out, prop.second.type ); + write< uint8_t >( out, prop.second.type ); switch ( prop.second.type ) { - case PropertyValue::Bool: write< sf::Uint8 >( out, prop.second.data.b ? 1 : 0 ); break; + case PropertyValue::Bool: write< uint8_t >( out, prop.second.data.b ? 1 : 0 ); break; case PropertyValue::Integer: write( out, prop.second.data.i ); break; case PropertyValue::Float: write( out, prop.second.data.f ); break; case PropertyValue::String: write( out, prop.second.dataStr ); break; @@ -130,10 +130,10 @@ namespace tbin ret.id = read< std::string >( in ); ret.desc = read< std::string >( in ); ret.image = read< std::string >( in ); - ret.sheetSize = read< sf::Vector2i >( in ); - ret.tileSize = read< sf::Vector2i >( in ); - ret.margin = read< sf::Vector2i >( in ); - ret.spacing = read< sf::Vector2i >( in ); + ret.sheetSize = read< tbin::Vector2i >( in ); + ret.tileSize = read< tbin::Vector2i >( in ); + ret.margin = read< tbin::Vector2i >( in ); + ret.spacing = read< tbin::Vector2i >( in ); ret.props = readProperties( in ); return ret; } @@ -154,8 +154,8 @@ namespace tbin { Tile ret; ret.tilesheet = currTilesheet; - ret.staticData.tileIndex = read< sf::Int32 >( in ); - ret.staticData.blendMode = read< sf::Uint8 >( in ); + ret.staticData.tileIndex = read< int32_t >( in ); + ret.staticData.blendMode = read< uint8_t >( in ); ret.props = readProperties( in ); return ret; } @@ -170,9 +170,9 @@ namespace tbin Tile readAnimatedTile( std::istream& in ) { Tile ret; - ret.animatedData.frameInterval = read< sf::Int32 >( in ); + ret.animatedData.frameInterval = read< int32_t >( in ); - int frameCount = read< sf::Int32 >( in ); + int frameCount = read< int32_t >( in ); ret.animatedData.frames.reserve( frameCount ); std::string currTilesheet; for ( int i = 0; i < frameCount; ) @@ -200,19 +200,19 @@ namespace tbin void writeAnimatedTile( std::ostream& out, const Tile& tile ) { write( out, tile.animatedData.frameInterval ); - write< sf::Int32 >( out, tile.animatedData.frames.size() ); + write< int32_t >( out, tile.animatedData.frames.size() ); std::string currTilesheet; for ( const Tile& frame : tile.animatedData.frames ) { if ( frame.tilesheet != currTilesheet ) { - write< sf::Uint8 >( out, 'T' ); + write< uint8_t >( out, 'T' ); write( out, frame.tilesheet ); currTilesheet = frame.tilesheet; } - write< sf::Uint8 >( out, 'S' ); + write< uint8_t >( out, 'S' ); writeStaticTile( out, frame ); } @@ -223,10 +223,10 @@ namespace tbin { Layer ret; ret.id = read< std::string >( in ); - ret.visible = read< sf::Uint8 >( in ) > 0; + ret.visible = read< uint8_t >( in ) > 0; ret.desc = read< std::string >( in ); - ret.layerSize = read< sf::Vector2i >( in ); - ret.tileSize = read< sf::Vector2i >( in ); + ret.layerSize = read< tbin::Vector2i >( in ); + ret.tileSize = read< tbin::Vector2i >( in ); ret.props = readProperties( in ); Tile nullTile; @@ -239,11 +239,11 @@ namespace tbin int ix = 0; while ( ix < ret.layerSize.x ) { - sf::Uint8 c = read< sf::Uint8 >( in ); + uint8_t c = read< uint8_t >( in ); switch ( c ) { case 'N': - ix += read< sf::Int32 >( in ); + ix += read< int32_t >( in ); break; case 'S': ret.tiles[ ix + iy * ret.layerSize.x ] = readStaticTile( in, currTilesheet ); @@ -268,7 +268,7 @@ namespace tbin void writeLayer( std::ostream& out, const Layer& layer ) { write( out, layer.id ); - write< sf::Uint8 >( out, layer.visible ? 1 : 0 ); + write< uint8_t >( out, layer.visible ? 1 : 0 ); write( out, layer.desc ); write( out, layer.layerSize ); write( out, layer.tileSize ); @@ -277,7 +277,7 @@ namespace tbin std::string currTilesheet = ""; for ( int iy = 0; iy < layer.layerSize.y; ++iy ) { - sf::Int32 nulls = 0; + int32_t nulls = 0; for ( int ix = 0; ix < layer.layerSize.x; ++ix ) { const Tile& tile = layer.tiles[ ix + iy * layer.layerSize.x ]; @@ -290,33 +290,33 @@ namespace tbin if ( nulls > 0 ) { - write< sf::Uint8 >( out, 'N' ); + write< uint8_t >( out, 'N' ); write( out, nulls ); nulls = 0; } if ( tile.tilesheet != currTilesheet ) { - write< sf::Uint8 >( out, 'T' ); + write< uint8_t >( out, 'T' ); write( out, tile.tilesheet ); currTilesheet = tile.tilesheet; } if ( tile.animatedData.frames.size() == 0 ) { - write< sf::Uint8 >( out, 'S' ); + write< uint8_t >( out, 'S' ); writeStaticTile( out, tile ); } else { - write< sf::Uint8 >( out, 'A' ); + write< uint8_t >( out, 'A' ); writeAnimatedTile( out, tile ); } } if ( nulls > 0 ) { - write< sf::Uint8 >( out, 'N' ); + write< uint8_t >( out, 'N' ); write( out, nulls ); } } @@ -351,14 +351,14 @@ namespace tbin Properties props = readProperties( in ); std::vector< TileSheet > tilesheets; - int tilesheetCount = read< sf::Int32 >( in ); + int tilesheetCount = read< int32_t >( in ); for ( int i = 0; i < tilesheetCount; ++i ) { tilesheets.push_back( readTilesheet( in ) ); } std::vector< Layer > layers; - int layerCount = read< sf::Int32 >( in ); + int layerCount = read< int32_t >( in ); for ( int i = 0; i < layerCount; ++i ) { layers.push_back( readLayer( in ) ); @@ -394,11 +394,11 @@ namespace tbin write( out, desc ); writeProperties( out, props ); - write< sf::Int32 >( out, tilesheets.size() ); + write< int32_t >( out, tilesheets.size() ); for ( const TileSheet& ts : tilesheets ) writeTilesheet( out, ts ); - write< sf::Int32 >( out, layers.size() ); + write< int32_t >( out, layers.size() ); for ( const Layer& layer : layers ) writeLayer( out, layer ); diff --git a/src/plugins/tbin/tbin/Map.hpp b/src/plugins/tbin/tbin/Map.hpp index 1ac119411c..3455390c9a 100644 --- a/src/plugins/tbin/tbin/Map.hpp +++ b/src/plugins/tbin/tbin/Map.hpp @@ -1,6 +1,6 @@ /* * TBIN - * Copyright 2017, Chase Warrington + * Copyright 2017, Casey Warrington * * MIT License * diff --git a/src/plugins/tbin/tbin/PropertyValue.hpp b/src/plugins/tbin/tbin/PropertyValue.hpp index 9f1fcb3b14..9b07dab9db 100644 --- a/src/plugins/tbin/tbin/PropertyValue.hpp +++ b/src/plugins/tbin/tbin/PropertyValue.hpp @@ -1,6 +1,6 @@ /* * TBIN - * Copyright 2017, Chase Warrington + * Copyright 2017, Casey Warrington * * MIT License * @@ -26,12 +26,11 @@ #ifndef TBIN_PROPERTYVALUE_HPP #define TBIN_PROPERTYVALUE_HPP +#include #include //#include #include -#include "FakeSfml.hpp" - namespace tbin { struct PropertyValue @@ -49,7 +48,7 @@ namespace tbin union { bool b; - sf::Int32 i; + int32_t i; float f; } data; std::string dataStr; diff --git a/src/plugins/tbin/tbin/Tile.hpp b/src/plugins/tbin/tbin/Tile.hpp index 215e32f4eb..b0a17efe7a 100644 --- a/src/plugins/tbin/tbin/Tile.hpp +++ b/src/plugins/tbin/tbin/Tile.hpp @@ -1,6 +1,6 @@ /* * TBIN - * Copyright 2017, Chase Warrington + * Copyright 2017, Casey Warrington * * MIT License * @@ -26,10 +26,10 @@ #ifndef TBIN_TILE_HPP #define TBIN_TILE_HPP +#include //#include #include -#include "FakeSfml.hpp" #include "PropertyValue.hpp" namespace tbin @@ -43,26 +43,25 @@ namespace tbin Static, Animated, }; - + std::string tilesheet; - + struct { - sf::Int32 tileIndex; - sf::Uint8 blendMode; + int32_t tileIndex = -1; + uint8_t blendMode = 0; } staticData; - + struct { - sf::Int32 frameInterval; + int32_t frameInterval = 0; std::vector< Tile > frames; } animatedData; - + Properties props; - + inline bool isNullTile() const { return staticData.tileIndex == -1 && animatedData.frames.size() == 0; } }; } #endif // TBIN_TILE_HPP - diff --git a/src/plugins/tbin/tbin/TileSheet.hpp b/src/plugins/tbin/tbin/TileSheet.hpp index 442f64482e..6e844d0812 100644 --- a/src/plugins/tbin/tbin/TileSheet.hpp +++ b/src/plugins/tbin/tbin/TileSheet.hpp @@ -1,6 +1,6 @@ /* * TBIN - * Copyright 2017, Chase Warrington + * Copyright 2017, Casey Warrington * * MIT License * @@ -29,7 +29,7 @@ //#include #include -#include "FakeSfml.hpp" +#include "Vector2i.hpp" #include "PropertyValue.hpp" namespace tbin @@ -40,10 +40,10 @@ namespace tbin std::string id; std::string desc; std::string image; - sf::Vector2i sheetSize; - sf::Vector2i tileSize; - sf::Vector2i margin; - sf::Vector2i spacing; + tbin::Vector2i sheetSize; + tbin::Vector2i tileSize; + tbin::Vector2i margin; + tbin::Vector2i spacing; Properties props; }; } diff --git a/src/plugins/tbin/tbin/Vector2i.hpp b/src/plugins/tbin/tbin/Vector2i.hpp new file mode 100644 index 0000000000..31f7b7313e --- /dev/null +++ b/src/plugins/tbin/tbin/Vector2i.hpp @@ -0,0 +1,18 @@ +#ifndef TBIN_VECTOR2I_HPP +#define TBIN_VECTOR2I_HPP + +#include + +namespace tbin +{ + struct Vector2i + { + int32_t x; + int32_t y; + + Vector2i() : x(0),y(0) {} + Vector2i( int32_t x, int32_t y ) : x(x), y(y) {} + }; +} + +#endif // TBIN_VECTOR2I_HPP diff --git a/src/plugins/tbin/tbin_global.h b/src/plugins/tbin/tbin_global.h index ee19439e25..9e555a4a5f 100644 --- a/src/plugins/tbin/tbin_global.h +++ b/src/plugins/tbin/tbin_global.h @@ -1,6 +1,6 @@ /* * Tbin Tiled Plugin - * Copyright 2017, Chase Warrington + * Copyright 2017, Casey Warrington * * This file is part of Tiled. * diff --git a/src/plugins/tbin/tbinmapformat.cpp b/src/plugins/tbin/tbinmapformat.cpp new file mode 100644 index 0000000000..88aba4a154 --- /dev/null +++ b/src/plugins/tbin/tbinmapformat.cpp @@ -0,0 +1,117 @@ +/* + * TBIN Tiled Plugin + * Copyright 2017, Casey Warrington + * + * This file is part of Tiled. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +#include "tbinmapformat.h" + +#include "tbinplugin.h" +#include "tbin/Map.hpp" + +#include "logginginterface.h" +#include "map.h" + +#include +#include +#include + +#include +#include + +namespace Tbin { + +TbinMapFormat::TbinMapFormat(QObject *parent) + : Tiled::MapFormat(parent) +{ +} + +std::unique_ptr TbinMapFormat::read(const QString &fileName) +{ + std::ifstream file( fileName.toStdString(), std::ios::in | std::ios::binary ); + if (!file) { + mError = QCoreApplication::translate("File Errors", "Could not open file for reading."); + return nullptr; + } + + try + { + tbin::Map tmap; + tmap.loadFromStream(file); + + const QDir fileDir(QFileInfo(fileName).dir()); + return TbinPlugin::fromTbin( tmap, fileDir ); + } + catch (std::exception& e) { + mError = tr((std::string("Exception: ") + e.what()).c_str()); + return nullptr; + } +} + +bool TbinMapFormat::write(const Tiled::Map *map, const QString &fileName, Options options) +{ + Q_UNUSED(options) + + try { + const QDir fileDir(QFileInfo(fileName).dir()); + tbin::Map tmap = TbinPlugin::toTbin( map, fileDir ); + + std::ofstream file(fileName.toStdString(), std::ios::trunc | std::ios::binary); + if (!file) { + mError = tr("Could not open file for writing"); + return false; + } + tmap.saveToStream(file); + file.close(); + + return true; + } + catch (std::exception& e) + { + mError = tr("Exception: %1").arg(tr(e.what())); + return false; + } +} + +QString TbinMapFormat::nameFilter() const +{ + return tr("tIDE binary map files (*.tbin)"); +} + +QString TbinMapFormat::shortName() const +{ + return QStringLiteral("tbin"); +} + +bool TbinMapFormat::supportsFile(const QString &fileName) const +{ + std::ifstream file(fileName.toStdString(), std::ios::in | std::ios::binary); + if (!file) + return false; + + std::string magic(6, '\0'); + file.read(&magic[0], static_cast(magic.length())); + + return magic == "tBIN10"; +} + +QString TbinMapFormat::errorString() const +{ + return mError; +} + +} // namespace Tbin diff --git a/src/plugins/tbin/tbinmapformat.h b/src/plugins/tbin/tbinmapformat.h new file mode 100644 index 0000000000..2ee7efa862 --- /dev/null +++ b/src/plugins/tbin/tbinmapformat.h @@ -0,0 +1,56 @@ +/* + * TBIN Tiled Plugin + * Copyright 2017, Casey Warrington + * + * This file is part of Tiled. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +#pragma once + +#include "tbin_global.h" + +#include "mapformat.h" + +#include + +namespace Tiled { +class Map; +} + +namespace Tbin { + +class TBINSHARED_EXPORT TbinMapFormat : public Tiled::MapFormat +{ + Q_OBJECT + Q_INTERFACES(Tiled::MapFormat) + +public: + TbinMapFormat(QObject *parent = nullptr); + + std::unique_ptr read(const QString &fileName) override; + bool supportsFile(const QString &fileName) const override; + + bool write(const Tiled::Map *map, const QString &fileName, Options options) override; + + QString nameFilter() const override; + QString shortName() const override; + QString errorString() const override; + +protected: + QString mError; +}; + +} // namespace Tbin diff --git a/src/plugins/tbin/tbinplugin.cpp b/src/plugins/tbin/tbinplugin.cpp index df88b974f4..d7b38a11f2 100644 --- a/src/plugins/tbin/tbinplugin.cpp +++ b/src/plugins/tbin/tbinplugin.cpp @@ -1,6 +1,6 @@ /* * TBIN Tiled Plugin - * Copyright 2017, Chase Warrington + * Copyright 2017, Casey Warrington * * This file is part of Tiled. * @@ -20,69 +20,68 @@ #include "tbinplugin.h" +#include "tbinmapformat.h" +#include "tidemapformat.h" #include "tbin/Map.hpp" #include "logginginterface.h" #include "map.h" #include "mapobject.h" #include "objectgroup.h" -#include "savefile.h" #include "tile.h" #include "tiled.h" #include "tilelayer.h" +#include + #include #include #include -#include -#include -#include -#include -#include - namespace { - void tbinToTiledProperties(const tbin::Properties &props, Tiled::Object &obj) + void tbinToTiledProperties(const tbin::Properties& props, Tiled::Object& obj) { - for (const auto &prop : props) { + for (const auto& prop : props) { if (prop.first[0] == '@') continue; switch (prop.second.type) { - case tbin::PropertyValue::String: - obj.setProperty(QString::fromStdString(prop.first), QString::fromStdString(prop.second.dataStr)); - break; + case tbin::PropertyValue::String: + obj.setProperty(QString::fromStdString(prop.first), QString::fromStdString(prop.second.dataStr)); + break; - case tbin::PropertyValue::Bool: - obj.setProperty(QString::fromStdString(prop.first), prop.second.data.b); - break; + case tbin::PropertyValue::Bool: + obj.setProperty(QString::fromStdString(prop.first), prop.second.data.b); + break; - case tbin::PropertyValue::Float: - obj.setProperty(QString::fromStdString(prop.first), prop.second.data.f); - break; + case tbin::PropertyValue::Float: + obj.setProperty(QString::fromStdString(prop.first), prop.second.data.f); + break; - case tbin::PropertyValue::Integer: - obj.setProperty(QString::fromStdString(prop.first), prop.second.data.i); - break; + case tbin::PropertyValue::Integer: + obj.setProperty(QString::fromStdString(prop.first), prop.second.data.i); + break; } } } - void tiledToTbinProperties(const Tiled::Properties &properties, tbin::Properties &tprops, const QDir &dir) + void tiledToTbinProperties(const Tiled::Properties& properties, tbin::Properties& tprops, const QDir& dir, bool allowOverwrite = false) { for (auto it = properties.cbegin(), it_end = properties.cend(); it != it_end; ++it) { - const auto &name = it.key(); + const auto& name = it.key(); auto value = it.value(); if (value.userType() == Tiled::filePathTypeId()) { Tiled::WARNING(QStringLiteral("tBIN: Saving 'file' property \"%1\" as 'string'.").arg(name)); const auto filePath = value.value(); value = Tiled::toFileReference(filePath.url, dir); - } else if (value.userType() == Tiled::objectRefTypeId()) { + } + else if (value.userType() == Tiled::objectRefTypeId()) { Tiled::WARNING(QStringLiteral("tBIN: Saving 'object' property \"%1\" as 'int'.").arg(name)); const auto objectRef = value.value(); value = Tiled::ObjectRef::toInt(objectRef); - } else if (value.userType() == QMetaType::QColor) { + } + else if (value.userType() == QMetaType::QColor) { Tiled::WARNING(QStringLiteral("tBIN: Saving 'color' property \"%1\" as 'string'.").arg(name)); const auto color = value.value(); value = color.isValid() ? color.name(QColor::HexArgb) : QString(); @@ -112,9 +111,23 @@ namespace throw std::invalid_argument(QT_TRANSLATE_NOOP("TbinMapFormat", "Unsupported property type")); } - tprops.insert(std::make_pair(name.toStdString(), prop)); + if (tprops.find( name.toStdString() ) != tprops.end()) + { + if ( allowOverwrite ) + tprops[name.toStdString()] = prop; + } + else + tprops.insert(std::make_pair(name.toStdString(), prop)); } } + + bool hasUnsupportedFlags(const Tiled::Cell& cell) + { + return cell.flippedHorizontally() || + cell.flippedVertically() || + cell.flippedAntiDiagonally() || + cell.rotatedHexagonal120(); + } } namespace Tbin { @@ -122,327 +135,354 @@ namespace Tbin { void TbinPlugin::initialize() { addObject(new TbinMapFormat(this)); + addObject(new TideMapFormat(this)); } - -TbinMapFormat::TbinMapFormat(QObject *) -{ -} - -std::unique_ptr TbinMapFormat::read(const QString &fileName) +std::unique_ptr< Tiled::Map > TbinPlugin::fromTbin( const tbin::Map& tmap, const QDir &fileDir ) { - std::ifstream file( fileName.toStdString(), std::ios::in | std::ios::binary ); - if (!file) { - mError = QCoreApplication::translate("File Errors", "Could not open file for reading."); - return nullptr; - } - - tbin::Map tmap; std::unique_ptr map; - try - { - tmap.loadFromStream(file); - if (tmap.layers.empty()) - throw std::invalid_argument(QT_TR_NOOP("Map contains no layers.")); + if (tmap.layers.empty()) + throw std::invalid_argument(QT_TR_NOOP("Map contains no layers.")); - auto &firstLayer = tmap.layers[0]; + auto &firstLayer = tmap.layers[0]; - Tiled::Map::Parameters mapParameters; - mapParameters.width = firstLayer.layerSize.x; - mapParameters.height = firstLayer.layerSize.y; - mapParameters.tileWidth = firstLayer.tileSize.x; - mapParameters.tileHeight = firstLayer.tileSize.y; + Tiled::Map::Parameters mapParameters; + mapParameters.width = firstLayer.layerSize.x; + mapParameters.height = firstLayer.layerSize.y; + mapParameters.tileWidth = firstLayer.tileSize.x; + mapParameters.tileHeight = firstLayer.tileSize.y; - map = std::make_unique(mapParameters); + map = std::make_unique(mapParameters); - tbinToTiledProperties(tmap.props, *map); + tbinToTiledProperties(tmap.props, *map); - const QDir fileDir(QFileInfo(fileName).dir()); + std::map< std::string, int > tmapTilesheetMapping; + for (std::size_t i = 0; i < tmap.tilesheets.size(); ++i) { + const tbin::TileSheet& ttilesheet = tmap.tilesheets[i]; + tmapTilesheetMapping[ttilesheet.id] = static_cast(i); - std::map< std::string, int > tmapTilesheetMapping; - for (std::size_t i = 0; i < tmap.tilesheets.size(); ++i) { - const tbin::TileSheet& ttilesheet = tmap.tilesheets[i]; - tmapTilesheetMapping[ttilesheet.id] = static_cast(i); + if (ttilesheet.spacing.x != ttilesheet.spacing.y) + throw std::invalid_argument(QT_TR_NOOP("Tilesheet must have equal spacings.")); + if (ttilesheet.margin.x != ttilesheet.margin.y) + throw std::invalid_argument(QT_TR_NOOP("Tilesheet must have equal margins.")); - if (ttilesheet.spacing.x != ttilesheet.spacing.y) - throw std::invalid_argument(QT_TR_NOOP("Tilesheet must have equal spacings.")); - if (ttilesheet.margin.x != ttilesheet.margin.y) - throw std::invalid_argument(QT_TR_NOOP("Tilesheet must have equal margins.")); + auto tileset = Tiled::Tileset::create(ttilesheet.id.c_str(), ttilesheet.tileSize.x, ttilesheet.tileSize.y, ttilesheet.spacing.x, ttilesheet.margin.x); + tileset->setImageSource(Tiled::toUrl(QString::fromStdString(ttilesheet.image).replace("\\", "/"), fileDir)); + tileset->loadImage(); - auto tileset = Tiled::Tileset::create(ttilesheet.id.c_str(), ttilesheet.tileSize.x, ttilesheet.tileSize.y, ttilesheet.spacing.x, ttilesheet.margin.x); - tileset->setImageSource(Tiled::toUrl(QString::fromStdString(ttilesheet.image).replace("\\", "/"), fileDir)); - tileset->loadImage(); + tbinToTiledProperties(ttilesheet.props, *tileset); - tbinToTiledProperties(ttilesheet.props, *tileset); + for (const auto &prop : ttilesheet.props) { + if (prop.first[0] != '@') + continue; - for (const auto &prop : ttilesheet.props) { - if (prop.first[0] != '@') - continue; + const QString name = QString::fromStdString(prop.first); + const auto strs = QStringView(name).split(QLatin1Char('@')); + if (strs[1] == QLatin1String("TileIndex")) { + int index = strs[2].toInt(); + tbin::Properties dummyProps; + dummyProps.insert(std::make_pair(strs[3].toUtf8().constData(), prop.second)); + Tiled::Tile *tile = tileset->findOrCreateTile(index); + tbinToTiledProperties(dummyProps, *tile); + } + // TODO: 'AutoTile' ? + // Purely for map making. Appears to be similar to terrains + // (In tIDE, right click a tilesheet and choose "Auto Tiles..." + } - const QString name = QString::fromStdString(prop.first); - const auto strs = QStringView(name).split(QLatin1Char('@')); - if (strs[1] == QLatin1String("TileIndex")) { - int index = strs[2].toInt(); - tbin::Properties dummyProps; - dummyProps.insert(std::make_pair(strs[3].toUtf8().constData(), prop.second)); - Tiled::Tile *tile = tileset->findOrCreateTile(index); - tbinToTiledProperties(dummyProps, *tile); + map->addTileset(tileset); + } + for (const tbin::Layer& tlayer : tmap.layers) { + if (tlayer.tileSize.x != firstLayer.tileSize.x || tlayer.tileSize.y != firstLayer.tileSize.y) + throw std::invalid_argument(QT_TR_NOOP("Different tile sizes per layer are not supported.")); + + auto layer = std::make_unique(QString::fromStdString(tlayer.id), 0, 0, tlayer.layerSize.x, tlayer.layerSize.y); + layer->setVisible( tlayer.visible ); + tbinToTiledProperties(tlayer.props, *layer); + auto objects = std::make_unique(QString::fromStdString(tlayer.id), 0, 0); + objects->setVisible( tlayer.visible ); + for (std::size_t i = 0; i < tlayer.tiles.size(); ++i) { + const tbin::Tile& ttile = tlayer.tiles[i]; + int ix = static_cast(i % static_cast(tlayer.layerSize.x)); + int iy = static_cast(i / static_cast(tlayer.layerSize.x)); + + if (ttile.isNullTile()) + continue; + + Tiled::Cell cell; + if (ttile.animatedData.frames.size() > 0) { + tbin::Tile tfirstTile = ttile.animatedData.frames[0]; + Tiled::Tile* firstTile = map->tilesetAt(tmapTilesheetMapping[tfirstTile.tilesheet])->findOrCreateTile(tfirstTile.staticData.tileIndex); + QVector frames; + for (const tbin::Tile& tframe : ttile.animatedData.frames) { + if (tframe.isNullTile() || tframe.animatedData.frames.size() > 0 || + tframe.tilesheet != tfirstTile.tilesheet) + throw std::invalid_argument(QT_TR_NOOP("Invalid animation frame.")); + + Tiled::Frame frame; + frame.tileId = tframe.staticData.tileIndex; + frame.duration = ttile.animatedData.frameInterval; + frames.append(frame); } - // TODO: 'AutoTile' ? - // Purely for map making. Appears to be similar to terrains - // (In tIDE, right click a tilesheet and choose "Auto Tiles..." + firstTile->setFrames(frames); + cell = Tiled::Cell(firstTile); + } + else { + cell = Tiled::Cell(map->tilesetAt(tmapTilesheetMapping[ttile.tilesheet]).data(), ttile.staticData.tileIndex); + } + layer->setCell(ix, iy, cell); + + if (ttile.props.size() > 0) + { + auto obj = std::make_unique("TileData"); + obj->setPosition(QPointF(ix * tlayer.tileSize.x, iy * tlayer.tileSize.y)); + obj->setSize(QSizeF(tlayer.tileSize.x, tlayer.tileSize.y)); + tbinToTiledProperties(ttile.props, *obj); + objects->addObject(std::move(obj)); } - - map->addTileset(tileset); } - for (const tbin::Layer& tlayer : tmap.layers) { - if (tlayer.tileSize.x != firstLayer.tileSize.x || tlayer.tileSize.y != firstLayer.tileSize.y) - throw std::invalid_argument(QT_TR_NOOP("Different tile sizes per layer are not supported.")); - - auto layer = std::make_unique(QString::fromStdString(tlayer.id), 0, 0, tlayer.layerSize.x, tlayer.layerSize.y); - tbinToTiledProperties(tlayer.props, *layer); - auto objects = std::make_unique(QString::fromStdString(tlayer.id), 0, 0); - for (std::size_t i = 0; i < tlayer.tiles.size(); ++i) { - const tbin::Tile& ttile = tlayer.tiles[i]; - int ix = static_cast(i % static_cast(tlayer.layerSize.x)); - int iy = static_cast(i / static_cast(tlayer.layerSize.x)); - - if (ttile.isNullTile()) - continue; - - Tiled::Cell cell; - if (ttile.animatedData.frames.size() > 0) { - tbin::Tile tfirstTile = ttile.animatedData.frames[0]; - Tiled::Tile* firstTile = map->tilesetAt(tmapTilesheetMapping[tfirstTile.tilesheet])->findOrCreateTile(tfirstTile.staticData.tileIndex); - QVector frames; - for (const tbin::Tile& tframe : ttile.animatedData.frames) { - if (tframe.isNullTile() || tframe.animatedData.frames.size() > 0 || - tframe.tilesheet != tfirstTile.tilesheet) - throw std::invalid_argument(QT_TR_NOOP("Invalid animation frame.")); - - Tiled::Frame frame; - frame.tileId = tframe.staticData.tileIndex; - frame.duration = ttile.animatedData.frameInterval; - frames.append(frame); + + bool doMergePass = true; + while ( doMergePass ) + { + doMergePass = false; + + // Consolidate duplicate TileData into a larger single TileData, if possible + // Not a perfectly efficient implementation as far as the results, but better than every tile being separate. + for ( const auto& obj : ( * objects ) ) + { + for ( const auto& otherObj : ( * objects ) ) + { + if ( &obj == &otherObj ) + continue; + + int myX = (int) obj->x() / tlayer.tileSize.x; + int myY = (int) obj->y() / tlayer.tileSize.y; + int myWidth = (int) obj->width() / tlayer.tileSize.x; + int myHeight = (int) obj->height() / tlayer.tileSize.y; + + int otherX = (int) otherObj->x() / tlayer.tileSize.x; + int otherY = (int) otherObj->y() / tlayer.tileSize.y; + int otherWidth = (int) otherObj->width() / tlayer.tileSize.x; + int otherHeight = (int) otherObj->height() / tlayer.tileSize.y; + + bool tryMerge = false; + int mergeW = 0; + int mergeH = 0; + + // Merge left if possible + if ( otherX + otherWidth == myX && otherY == myY && otherHeight == myHeight ) + { + tryMerge = true; + mergeW = 1; } - firstTile->setFrames(frames); - cell = Tiled::Cell(firstTile); - } - else { - cell = Tiled::Cell(map->tilesetAt(tmapTilesheetMapping[ttile.tilesheet]).data(), ttile.staticData.tileIndex); + // Merge up if possible + else if ( otherX == myX && otherY + otherHeight == myY && otherWidth == myWidth ) + { + tryMerge = true; + mergeH = 1; + } + + if ( !tryMerge ) + continue; + + Tiled::Properties& myProps = obj->properties(); + Tiled::Properties& otherProps = otherObj->properties(); + + // Make sure that we only merge if the target is strictly a subset of this tile + bool anyMissingOrNonmatching = false; + for ( const auto& propKey : otherProps.keys() ) + { + if ( !myProps.contains( propKey ) || myProps[ propKey ] != otherProps[ propKey ] ) + { + anyMissingOrNonmatching = true; + break; + } + } + + if ( anyMissingOrNonmatching ) + continue; + + // Merge what we can + for ( const auto& propKey : otherProps.keys() ) + { + myProps.remove( propKey ); + } + otherObj->setSize( (otherWidth + mergeW) * tlayer.tileSize.x, (otherHeight + mergeH) * tlayer.tileSize.y ); + + doMergePass = true; + break; } - layer->setCell(ix, iy, cell); - - if (ttile.props.size() > 0) { - auto obj = std::make_unique("TileData"); - obj->setPosition(QPointF(ix * tlayer.tileSize.x, iy * tlayer.tileSize.y)); - obj->setSize(QSizeF(tlayer.tileSize.x, tlayer.tileSize.y)); - tbinToTiledProperties(ttile.props, *obj); - objects->addObject(std::move(obj)); + + if ( doMergePass ) + { + // If there's nothing left in this object after merging, remove it + if ( obj->properties().size() == 0 ) + objects->removeObject( obj ); + + break; } } - map->addLayer(std::move(layer)); - map->addLayer(std::move(objects)); } - } - catch (std::exception& e) { - mError = tr((std::string("Exception: ") + e.what()).c_str()); + map->addLayer(std::move(layer)); + map->addLayer(std::move(objects)); } return map; } -static bool hasFlags(const Tiled::Cell &cell) +tbin::Map TbinPlugin::toTbin( const Tiled::Map* map, const QDir &fileDir ) { - return cell.flippedHorizontally() || - cell.flippedVertically() || - cell.flippedAntiDiagonally() || - cell.rotatedHexagonal120(); -} + tbin::Map tmap; -bool TbinMapFormat::write(const Tiled::Map *map, const QString &fileName, Options options) -{ - Q_UNUSED(options) - - try { - const QDir fileDir(QFileInfo(fileName).dir()); - - tbin::Map tmap; - //tmap.id = map->name(); - tiledToTbinProperties(map->properties(), tmap.props, fileDir); - - for (const Tiled::SharedTileset& tilesheet : map->tilesets()) { - tbin::TileSheet ttilesheet; - ttilesheet.id = tilesheet->name().toStdString(); - ttilesheet.image = Tiled::toFileReference(tilesheet->imageSource(), fileDir).replace("/", "\\").toStdString(); - ttilesheet.margin.x = ttilesheet.margin.y = tilesheet->margin(); - ttilesheet.spacing.x = ttilesheet.spacing.y = tilesheet->tileSpacing(); - ttilesheet.sheetSize.x = tilesheet->columnCount(); - ttilesheet.sheetSize.y = tilesheet->rowCount(); - ttilesheet.tileSize.x = tilesheet->tileSize().width(); - ttilesheet.tileSize.y = tilesheet->tileSize().height(); - tiledToTbinProperties(tilesheet->properties(), ttilesheet.props, fileDir); - - Tiled::Properties tilesetTileProperties; - for (auto tile : tilesheet->tiles()) { - const auto &props = tile->properties(); - for (auto it = props.begin(), it_end = props.end(); it != it_end; ++it) { - tilesetTileProperties.insert("@TileIndex@" + QString::number(tile->id()) + "@" + it.key(), it.value()); - } + //tmap.id = map->name(); + tiledToTbinProperties(map->properties(), tmap.props, fileDir); + + for (const Tiled::SharedTileset& tilesheet : map->tilesets()) { + tbin::TileSheet ttilesheet; + ttilesheet.id = tilesheet->name().toStdString(); + ttilesheet.image = Tiled::toFileReference(tilesheet->imageSource(), fileDir).replace("/", "\\").toStdString(); + ttilesheet.margin.x = ttilesheet.margin.y = tilesheet->margin(); + ttilesheet.spacing.x = ttilesheet.spacing.y = tilesheet->tileSpacing(); + ttilesheet.sheetSize.x = tilesheet->columnCount(); + ttilesheet.sheetSize.y = tilesheet->rowCount(); + ttilesheet.tileSize.x = tilesheet->tileSize().width(); + ttilesheet.tileSize.y = tilesheet->tileSize().height(); + tiledToTbinProperties(tilesheet->properties(), ttilesheet.props, fileDir); + + Tiled::Properties tilesetTileProperties; + for (auto tile : tilesheet->tiles()) { + const auto &props = tile->properties(); + for (auto it = props.begin(), it_end = props.end(); it != it_end; ++it) { + tilesetTileProperties.insert("@TileIndex@" + QString::number(tile->id()) + "@" + it.key(), it.value()); } - tiledToTbinProperties(tilesetTileProperties, ttilesheet.props, fileDir); + } + tiledToTbinProperties(tilesetTileProperties, ttilesheet.props, fileDir); + + tmap.tilesheets.push_back(std::move(ttilesheet)); + } - tmap.tilesheets.push_back(std::move(ttilesheet)); + std::vector< Tiled::ObjectGroup* > objGroups; + std::map< std::string, tbin::Layer* > tileLayerIdMap; + tmap.layers.reserve(static_cast(map->layers().size())); + for (Tiled::Layer* rawLayer : map->layers()) { + if (Tiled::ObjectGroup* layer = rawLayer->asObjectGroup()) { + objGroups.push_back(layer); } + else if (Tiled::TileLayer* layer = rawLayer->asTileLayer()) { + tbin::Layer tlayer; + tlayer.id = layer->name().toStdString(); + tlayer.layerSize.x = layer->width(); + tlayer.layerSize.y = layer->height(); + tlayer.tileSize.x = map->tileWidth(); + tlayer.tileSize.y = map->tileHeight(); + tlayer.visible = layer->isVisible(); + for (int iy = 0; iy < tlayer.layerSize.y; ++iy) { + for (int ix = 0; ix < tlayer.layerSize.x; ++ix) { + Tiled::Cell cell = layer->cellAt(ix, iy); + tbin::Tile ttile; + ttile.staticData.tileIndex = -1; + + if (hasUnsupportedFlags(cell)) { + Tiled::ERROR("tBIN: Flipped and/or rotated tiles are not supported.", + Tiled::JumpToTile { map, QPoint(ix + layer->x(), iy + layer->y()), layer }); + } - std::vector< Tiled::ObjectGroup* > objGroups; - std::map< std::string, tbin::Layer* > tileLayerIdMap; - tmap.layers.reserve(static_cast(map->layers().size())); - for (Tiled::Layer* rawLayer : map->layers()) { - if (Tiled::ObjectGroup* layer = rawLayer->asObjectGroup()) { - objGroups.push_back(layer); - } - else if (Tiled::TileLayer* layer = rawLayer->asTileLayer()) { - tbin::Layer tlayer; - tlayer.id = layer->name().toStdString(); - tlayer.layerSize.x = layer->width(); - tlayer.layerSize.y = layer->height(); - tlayer.tileSize.x = map->tileWidth(); - tlayer.tileSize.y = map->tileHeight(); - //tlayer.visible = ???; - for (int iy = 0; iy < tlayer.layerSize.y; ++iy) { - for (int ix = 0; ix < tlayer.layerSize.x; ++ix) { - Tiled::Cell cell = layer->cellAt(ix, iy); - tbin::Tile ttile; - ttile.staticData.tileIndex = -1; - - if (hasFlags(cell)) { - Tiled::ERROR("tBIN: Flipped and/or rotated tiles are not supported.", - Tiled::JumpToTile { map, QPoint(ix + layer->x(), iy + layer->y()), layer }); + if (Tiled::Tile *tile = cell.tile()) { + ttile.tilesheet = tile->tileset()->name().toStdString(); + if (tile->frames().size() == 0) { + ttile.staticData.tileIndex = tile->id(); + ttile.staticData.blendMode = layer->blendMode() == Tiled::BlendMode::Add ? 1 : 0; } + else { + ttile.animatedData.frameInterval = tile->frames().at(0).duration; - if (Tiled::Tile *tile = cell.tile()) { - ttile.tilesheet = tile->tileset()->name().toStdString(); - if (tile->frames().size() == 0) { - ttile.staticData.tileIndex = tile->id(); - ttile.staticData.blendMode = 0; - } - else { - ttile.animatedData.frameInterval = tile->frames().at(0).duration; - - for (Tiled::Frame frame : tile->frames()) { - if (frame.duration != ttile.animatedData.frameInterval) { - Tiled::ERROR("tBIN: Frames with different duration are not supported.", - Tiled::SelectTile { tile }); - } - - tbin::Tile tframe; - tframe.tilesheet = ttile.tilesheet; - tframe.staticData.tileIndex = frame.tileId; - tframe.staticData.blendMode = 0; - ttile.animatedData.frames.push_back(tframe); + for (Tiled::Frame frame : tile->frames()) { + if (frame.duration != ttile.animatedData.frameInterval) { + Tiled::ERROR("tBIN: Frames with different duration are not supported.", + Tiled::SelectTile { tile }); } + + tbin::Tile tframe; + tframe.tilesheet = ttile.tilesheet; + tframe.staticData.tileIndex = frame.tileId; + tframe.staticData.blendMode = layer->blendMode() == Tiled::BlendMode::Add ? 1 : 0; + ttile.animatedData.frames.push_back(tframe); } } - tlayer.tiles.push_back(ttile); } + tlayer.tiles.push_back(ttile); } - tiledToTbinProperties(layer->properties(), tlayer.props, fileDir); - tmap.layers.push_back(std::move(tlayer)); - tileLayerIdMap[tmap.layers.back().id] = &tmap.layers.back(); - } - else { - throw std::invalid_argument(QT_TR_NOOP("Only object and tile layers supported.")); } + tiledToTbinProperties(layer->properties(), tlayer.props, fileDir); + tmap.layers.push_back(std::move(tlayer)); + tileLayerIdMap[tmap.layers.back().id] = &tmap.layers.back(); } + else { + throw std::invalid_argument(QT_TR_NOOP("Only object and tile layers supported.")); + } + } + + for (Tiled::ObjectGroup* objs : objGroups) { + const auto groupName = objs->name(); - for (Tiled::ObjectGroup* objs : objGroups) { - const auto groupName = objs->name(); + tbin::Layer* tiles = tileLayerIdMap[groupName.toStdString()]; + if (!tiles) { + Tiled::WARNING(QStringLiteral("tBIN: Ignoring object layer \"%1\" without matching tile layer.").arg(groupName), + Tiled::SelectLayer { objs }); + continue; + } - tbin::Layer* tiles = tileLayerIdMap[groupName.toStdString()]; - if (!tiles) { - Tiled::WARNING(QStringLiteral("tBIN: Ignoring object layer \"%1\" without matching tile layer.").arg(groupName), - Tiled::SelectLayer { objs }); + for (Tiled::MapObject* obj : objs->objects()) { + if (obj->name() != QLatin1String("TileData")) { + Tiled::WARNING(QStringLiteral("tBIN: Ignoring object %1 with name different from 'TileData'.").arg(obj->id()), + Tiled::JumpToObject { obj }); continue; } - for (Tiled::MapObject* obj : objs->objects()) { - if (obj->name() != QLatin1String("TileData")) { - Tiled::WARNING(QStringLiteral("tBIN: Ignoring object %1 with name different from 'TileData'.").arg(obj->id()), - Tiled::JumpToObject { obj }); - continue; - } + if (obj->properties().isEmpty()) { + Tiled::WARNING(QStringLiteral("tBIN: Ignoring object %1 without custom properties.").arg(obj->id()), + Tiled::JumpToObject { obj }); + continue; + } - if (obj->properties().isEmpty()) { - Tiled::WARNING(QStringLiteral("tBIN: Ignoring object %1 without custom properties.").arg(obj->id()), - Tiled::JumpToObject { obj }); - continue; - } + if (obj->shape() != Tiled::MapObject::Shape::Rectangle) { + Tiled::WARNING(QStringLiteral("tBIN: Ignoring object %1 that isn't a rectangle.").arg(obj->id()), + Tiled::JumpToObject { obj }); + continue; + } - if (static_cast(obj->width()) != tiles->tileSize.x || - static_cast(obj->height()) != tiles->tileSize.y || - obj->x() / tiles->tileSize.x != std::floor(obj->x() / tiles->tileSize.x) || - obj->y() / tiles->tileSize.y != std::floor(obj->y() / tiles->tileSize.y)) { - Tiled::WARNING(QStringLiteral("tBIN: Object %1 is not aligned to the tile grid.").arg(obj->id()), - Tiled::JumpToObject { obj }); - } + if (static_cast(obj->width()) % tiles->tileSize.x != 0 || + static_cast(obj->height()) % tiles->tileSize.y != 0 || + obj->x() / tiles->tileSize.x != std::floor(obj->x() / tiles->tileSize.x) || + obj->y() / tiles->tileSize.y != std::floor(obj->y() / tiles->tileSize.y)) { + Tiled::WARNING(QStringLiteral("tBIN: Object %1 is not aligned to the tile grid.").arg(obj->id()), + Tiled::JumpToObject { obj }); + } + + for ( int iw = 0; iw < (int) obj->width() / tiles->tileSize.x; ++iw ) + { + for ( int ih = 0; ih < (int) obj->height() / tiles->tileSize.y; ++ih ) + { + int tileX = (int) obj->x() / tiles->tileSize.x + iw; + int tileY = (int) obj->y() / tiles->tileSize.y + ih; - // Determine tile position based on the center of the object - int tileX = static_cast(std::floor((obj->x() + (obj->width() / 2)) / tiles->tileSize.x)); - int tileY = static_cast(std::floor((obj->y() + (obj->height() / 2)) / tiles->tileSize.y)); + // Make sure the object is within the map boundaries (also makes sure values are positive) + if ( tileX < 0 || tileY < 0 || tileX >= tiles->layerSize.x || tileY >= tiles->layerSize.y ) + continue; - // Make sure the object is within the map boundaries (also makes sure values are positive) - tileX = qBound(0, tileX, tiles->layerSize.x - 1); - tileY = qBound(0, tileY, tiles->layerSize.y - 1); + bool isSingleTile = (int) obj->width() == tiles->tileSize.x && (int) obj->width() == tiles->tileSize.x; - std::size_t idx = static_cast(tileX + tileY * tiles->layerSize.x); - tiledToTbinProperties(obj->properties(), tiles->tiles[idx].props, fileDir); + std::size_t idx = static_cast(tileX + tileY * tiles->layerSize.x); + tiledToTbinProperties(obj->properties(), tiles->tiles[idx].props, fileDir, isSingleTile); + } } - } - std::ofstream file(fileName.toStdString(), std::ios::trunc | std::ios::binary); - if (!file) { - mError = tr("Could not open file for writing"); - return false; } - tmap.saveToStream(file); - file.close(); - } - catch (std::exception& e) - { - mError = tr("Exception: %1").arg(tr(e.what())); - return false; } - return true; -} - -QString TbinMapFormat::nameFilter() const -{ - return tr("Tbin map files (*.tbin)"); -} - -QString TbinMapFormat::shortName() const -{ - return QStringLiteral("tbin"); -} - -bool TbinMapFormat::supportsFile(const QString &fileName) const -{ - std::ifstream file(fileName.toStdString(), std::ios::in | std::ios::binary); - if (!file) - return false; - - std::string magic(6, '\0'); - file.read(&magic[0], static_cast(magic.length())); - - return magic == "tBIN10"; -} - -QString TbinMapFormat::errorString() const -{ - return mError; + return tmap; } } // namespace Tbin diff --git a/src/plugins/tbin/tbinplugin.h b/src/plugins/tbin/tbinplugin.h index 56d20e9208..593b1cb91d 100644 --- a/src/plugins/tbin/tbinplugin.h +++ b/src/plugins/tbin/tbinplugin.h @@ -1,6 +1,6 @@ /* * TBIN Tiled Plugin - * Copyright 2017, Chase Warrington + * Copyright 2017, Casey Warrington * * This file is part of Tiled. * @@ -24,11 +24,20 @@ #include "mapformat.h" #include "plugin.h" +#include "properties.h" #include -namespace Tiled { -class Map; +namespace Tiled +{ + class Object; + class Cell; + class Map; +} + +namespace tbin +{ + class Map; } namespace Tbin { @@ -41,28 +50,9 @@ class TBINSHARED_EXPORT TbinPlugin : public Tiled::Plugin public: void initialize() override; -}; - - -class TBINSHARED_EXPORT TbinMapFormat : public Tiled::MapFormat -{ - Q_OBJECT - Q_INTERFACES(Tiled::MapFormat) - -public: - TbinMapFormat(QObject *parent = nullptr); - - std::unique_ptr read(const QString &fileName) override; - bool supportsFile(const QString &fileName) const override; - - bool write(const Tiled::Map *map, const QString &fileName, Options options) override; - - QString nameFilter() const override; - QString shortName() const override; - QString errorString() const override; -protected: - QString mError; + static std::unique_ptr< Tiled::Map > fromTbin( const tbin::Map& tmap, const QDir &fileDir ); + static tbin::Map toTbin( const Tiled::Map* map, const QDir &fileDir ); }; -} // namespace Json +} // namespace Tbin diff --git a/src/plugins/tbin/tidemapformat.cpp b/src/plugins/tbin/tidemapformat.cpp new file mode 100644 index 0000000000..b12b75d04c --- /dev/null +++ b/src/plugins/tbin/tidemapformat.cpp @@ -0,0 +1,556 @@ +/* + * TBIN Tiled Plugin + * Copyright 2017, Casey Warrington + * + * This file is part of Tiled. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +#include "tidemapformat.h" + +#include "tbinplugin.h" +#include "tbin/Map.hpp" + +#include "logginginterface.h" +#include "map.h" + +#include +#include +#include +#include + +#include + +namespace Tbin { + +TideMapFormat::TideMapFormat(QObject *parent) + : Tiled::MapFormat(parent) +{ +} + +std::unique_ptr TideMapFormat::read(const QString &fileName) +{ + QFile file( fileName ); + if ( !file.exists() || !file.open( QFile::ReadOnly | QFile::Text ) ) + { + mError = QCoreApplication::translate("File Errors", "Could not open file for reading."); + return nullptr; + } + + try + { + QXmlStreamReader xml( &file ); + if ( !xml.readNextStartElement() || xml.name() != QLatin1String( "Map" ) ) + { + mError = QCoreApplication::translate("File Errors", "Not a tide file."); + return nullptr; + } + + tbin::Map tmap; + + const QXmlStreamAttributes mapAttrs = xml.attributes(); + tmap.id = mapAttrs.value( "Id" ).toString().toStdString(); + + auto readProps = [&xml]() -> tbin::Properties + { + tbin::Properties props; + + while ( xml.readNextStartElement() ) + { + if ( xml.name() == "Property" ) + { + const QXmlStreamAttributes attrs = xml.attributes(); + + const std::string key = attrs.value( "Key" ).toString().toStdString(); + const QString type = attrs.value( "Type" ).toString(); + const QString value = xml.readElementText(); + + tbin::PropertyValue prop; + if ( type == "Boolean" ) + { + prop.type = tbin::PropertyValue::Bool; + prop.data.b = QString::compare( value, "true", Qt::CaseInsensitive ) == 0; + } + if ( type == "Int32" ) + { + prop.type = tbin::PropertyValue::Integer; + prop.data.i = value.toInt(); + } + if ( type == "Single" ) + { + prop.type = tbin::PropertyValue::Float; + prop.data.f = value.toFloat(); + } + if ( type == "String" ) + { + prop.type = tbin::PropertyValue::String; + prop.dataStr = value.toStdString(); + } + + props.insert( std::make_pair( key, prop ) ); + } + else xml.skipCurrentElement(); + } + + return props; + }; + + while ( xml.readNextStartElement() ) + { + if ( xml.name() == QLatin1String( "Description" ) ) + tmap.desc = xml.readElementText().toStdString(); + else if ( xml.name() == "TileSheets" ) + { + while ( xml.readNextStartElement() ) + { + if ( xml.name() != "TileSheet" ) + { + xml.skipCurrentElement(); + continue; + } + + const QXmlStreamAttributes tsAttrs = xml.attributes(); + + tbin::TileSheet ts; + ts.id = tsAttrs.value( "Id" ).toString().toStdString(); + + while ( xml.readNextStartElement() ) + { + if ( xml.name() == QLatin1String( "Description" ) ) + ts.desc = xml.readElementText().toStdString(); + else if ( xml.name() == "ImageSource" ) + ts.image = xml.readElementText().toStdString(); + else if ( xml.name() == "Alignment" ) + { + const QXmlStreamAttributes alignAttrs = xml.attributes(); + + const QString sheetSizeStr = alignAttrs.value( "SheetSize" ).toString(); + const QString tileSizeStr = alignAttrs.value( "TileSize" ).toString(); + const QString marginStr = alignAttrs.value( "Margin" ).toString(); + const QString spacingStr = alignAttrs.value( "Spacing" ).toString(); + + qsizetype sheetSizeSep = sheetSizeStr.indexOf( " x " ); + qsizetype tileSizeSep = tileSizeStr.indexOf( " x " ); + qsizetype marginSep = marginStr.indexOf( " x " ); + qsizetype spacingSep = spacingStr.indexOf( " x " ); + + ts.sheetSize = tbin::Vector2i( sheetSizeStr.mid( 0, sheetSizeSep ).toInt(), sheetSizeStr.mid( sheetSizeSep + 3 ).toInt() ); + ts.tileSize = tbin::Vector2i( tileSizeStr.mid( 0, tileSizeSep ).toInt(), tileSizeStr.mid( tileSizeSep + 3 ).toInt() ); + ts.margin = tbin::Vector2i( marginStr.mid( 0, marginSep ).toInt(), marginStr.mid( marginSep + 3 ).toInt() ); + ts.spacing = tbin::Vector2i( spacingStr.mid( 0, spacingSep ).toInt(), spacingStr.mid( spacingSep + 3 ).toInt() ); + + xml.skipCurrentElement(); + } + else if (xml.name() == "Properties") + ts.props = readProps(); + else xml.skipCurrentElement(); + } + + tmap.tilesheets.push_back( ts ); + } + } + else if ( xml.name() == "Layers" ) + { + while ( xml.readNextStartElement() ) + { + if ( xml.name() != "Layer" ) + { + xml.skipCurrentElement(); + continue; + } + const QXmlStreamAttributes layerAttrs = xml.attributes(); + + tbin::Layer layer; + layer.id = layerAttrs.value( "Id" ).toString().toStdString(); + auto vis = layerAttrs.value( "Visible" ).toString().toStdString(); + layer.visible = QString::compare( layerAttrs.value( "Visible" ).toString(), "true", Qt::CaseInsensitive ) == 0; + + while ( xml.readNextStartElement() ) + { + if ( xml.name() == QLatin1String( "Description" ) ) + layer.desc = xml.readElementText().toStdString(); + else if ( xml.name() == "Dimensions" ) + { + const QXmlStreamAttributes alignAttrs = xml.attributes(); + + const QString layerSizeStr = alignAttrs.value( "LayerSize" ).toString(); + const QString tileSizeStr = alignAttrs.value( "TileSize" ).toString(); + + qsizetype layerSizeSep = layerSizeStr.indexOf( " x " ); + qsizetype tileSizeSep = tileSizeStr.indexOf( " x " ); + + layer.layerSize = tbin::Vector2i( layerSizeStr.mid( 0, layerSizeSep ).toInt(), layerSizeStr.mid( layerSizeSep + 3 ).toInt() ); + layer.tileSize = tbin::Vector2i( tileSizeStr.mid( 0, tileSizeSep ).toInt(), tileSizeStr.mid( tileSizeSep + 3 ).toInt() ); + + xml.skipCurrentElement(); + } + else if ( xml.name() == "TileArray" ) + { + tbin::Tile nullTile; + nullTile.staticData.tileIndex = -1; + layer.tiles.resize( layer.layerSize.x * layer.layerSize.y, nullTile ); + + auto readStaticTile = [&xml, &readProps](const std::string& tilesheetToUse)-> tbin::Tile + { + const QXmlStreamAttributes tileAttrs = xml.attributes(); + + tbin::Tile tile; + tile.tilesheet = tilesheetToUse; + tile.staticData.tileIndex = tileAttrs.value( "Index" ).toString().toInt(); + tile.staticData.blendMode = tileAttrs.value( "BlendMode" ).toString() == "Alpha" ? 0 : 1; + + // tIDE (the xTile tile editor) uses a self-closing element when there are no properties. + // However, Qt seems to represent these simply as empty elements when reading. + // We can just iterate as normal for reading. + // When writing, we'll still need to make sure it's an actual self-closing element, + // for compatibility with xTile code. + while ( xml.readNextStartElement() ) + { + if (xml.name() == "Properties") + tile.props = readProps(); + else xml.skipCurrentElement(); + } + + return tile; + }; + + std::string lastTilesheet; + tbin::Vector2i tilePos; + while ( xml.readNextStartElement() ) + { + if (xml.name() != "Row" ) + { + xml.skipCurrentElement(); + continue; + } + + while ( xml.readNextStartElement() ) + { + if ( xml.name() == "TileSheet" ) + { + const QXmlStreamAttributes tsAttrs = xml.attributes(); + lastTilesheet = tsAttrs.value( "Ref" ).toString().toStdString(); + xml.skipCurrentElement(); + } + else if ( xml.name() == "Static" ) + { + layer.tiles[ tilePos.x + tilePos.y * layer.layerSize.x ] = readStaticTile( lastTilesheet ); + tilePos.x += 1; + } + else if ( xml.name() == "Animated" ) + { + const QXmlStreamAttributes tileAttrs = xml.attributes(); + + tbin::Tile tile; + tile.animatedData.frameInterval = tileAttrs.value( "Interval" ).toString().toInt(); + + while ( xml.readNextStartElement() ) + { + if (xml.name() == "Frames" ) + { + std::string lastTilesheet; // Shadowing the previous declaration intentionally, because we explicitly do not want to use/affect it + while ( xml.readNextStartElement() ) + { + if ( xml.name() == "TileSheet" ) + { + const QXmlStreamAttributes tsAttrs = xml.attributes(); + lastTilesheet = tsAttrs.value( "Ref" ).toString().toStdString(); + xml.skipCurrentElement(); + } + else if (xml.name() == "Static") + tile.animatedData.frames.push_back( readStaticTile( lastTilesheet ) ); + else xml.skipCurrentElement(); + } + } + else if (xml.name() == "Properties") + tile.props = readProps(); + else xml.skipCurrentElement(); + } + + layer.tiles[ tilePos.x + tilePos.y * layer.layerSize.x ] = tile; + tilePos.x += 1; + } + else if ( xml.name() == "Null" ) + { + const QXmlStreamAttributes tileAttrs = xml.attributes(); + tilePos.x += tileAttrs.value( "Count" ).toString().toInt(); + xml.skipCurrentElement(); + } + else xml.skipCurrentElement(); + } + + tilePos.x = 0; + tilePos.y += 1; + } + } + else if (xml.name() == "Properties") + layer.props = readProps(); + else xml.skipCurrentElement(); + } + + tmap.layers.push_back( layer ); + } + } + else if (xml.name() == "Properties") + tmap.props = readProps(); + else xml.skipCurrentElement(); + } + + const QDir fileDir(QFileInfo(fileName).dir()); + return TbinPlugin::fromTbin( tmap, fileDir ); + } + catch (std::exception& e) { + mError = tr((std::string("Exception: ") + e.what()).c_str()); + return nullptr; + } +} + +bool TideMapFormat::write(const Tiled::Map *map, const QString &fileName, Options options) +{ + Q_UNUSED(options) + + QFile file( fileName ); + if ( !file.open( QFile::WriteOnly | QFile::Text | QFile::Truncate ) ) + { + mError = QCoreApplication::translate("File Errors", "Could not open file for writing."); + return false; + } + + try { + const QDir fileDir(QFileInfo(fileName).dir()); + tbin::Map tmap = TbinPlugin::toTbin( map, fileDir ); + + QXmlStreamWriter xml; + xml.setDevice( &file ); + xml.setAutoFormatting(true); // Not strictly necessary, but tIDE outputs formatted ones as well. Plus, it makes diffs easier + xml.setAutoFormattingIndent( 2 ); // Not strictly necessary, but good for matching existing tIDE files for diff purposes (or at least, for the test files I used). + + auto writeProps = [&xml](const tbin::Properties& props) + { + xml.writeStartElement( "Properties" ); + for ( const auto& prop : props ) + { + xml.writeStartElement( "Property" ); + xml.writeAttribute( "Key", QString(prop.first.c_str()) ); + switch ( prop.second.type ) + { + case tbin::PropertyValue::Bool: + xml.writeAttribute( "Type", "Boolean" ); + xml.writeCDATA( prop.second.data.b ? "True" : "False" ); + break; + case tbin::PropertyValue::Integer: + xml.writeAttribute( "Type", "Int32" ); + xml.writeCDATA( QString::number( prop.second.data.i ) ); + break; + case tbin::PropertyValue::Float: + xml.writeAttribute( "Type", "Single" ); + xml.writeCDATA( QString::number( prop.second.data.f ) ); + break; + case tbin::PropertyValue::String: + xml.writeAttribute( "Type", "String" ); + xml.writeCDATA( QString(prop.second.dataStr.c_str()) ); + break; + default: + throw std::invalid_argument( "Unknown property type" ); + } + xml.writeEndElement(); + } + xml.writeEndElement(); + }; + + xml.writeStartDocument(); + xml.writeStartElement( "Map" ); + { + xml.writeAttribute( "Id", QString(tmap.id.c_str()) ); + + xml.writeStartElement( "Description" ); + xml.writeCDATA( QString(tmap.desc.c_str()) ); + xml.writeEndElement(); + + xml.writeStartElement("TileSheets"); + { + for ( const auto& ts : tmap.tilesheets ) + { + xml.writeStartElement( "TileSheet" ); + xml.writeAttribute( "Id", QString(ts.id.c_str()) ); + + xml.writeStartElement( "Description" ); + xml.writeCDATA( QString(ts.desc.c_str()) ); + xml.writeEndElement(); + + xml.writeStartElement( "ImageSource" ); + xml.writeCDATA( QString(ts.image.c_str()) ); + xml.writeEndElement(); + + xml.writeEmptyElement( "Alignment" ); + xml.writeAttribute( "SheetSize", QStringLiteral( "%1 x %2" ).arg( ts.sheetSize.x ).arg( ts.sheetSize.y ) ); + xml.writeAttribute( "TileSize", QStringLiteral( "%1 x %2" ).arg( ts.tileSize.x ).arg( ts.tileSize.y ) ); + xml.writeAttribute( "Margin", QStringLiteral( "%1 x %2" ).arg( ts.margin.x ).arg( ts.margin.y ) ); + xml.writeAttribute( "Spacing", QStringLiteral( "%1 x %2" ).arg( ts.spacing.x ).arg( ts.spacing.y ) ); + + writeProps( ts.props ); + + xml.writeEndElement(); + } + } + xml.writeEndElement(); + + xml.writeStartElement("Layers"); + { + for ( const auto& layer : tmap.layers ) + { + xml.writeStartElement( "Layer" ); + xml.writeAttribute( "Id", QString(layer.id.c_str()) ); + xml.writeAttribute( "Visible", layer.visible ? "True" : "False" ); + + xml.writeStartElement( "Description" ); + xml.writeCDATA( QString(layer.desc.c_str()) ); + xml.writeEndElement(); + + xml.writeEmptyElement( "Dimensions" ); + xml.writeAttribute( "LayerSize", QStringLiteral( "%1 x %2" ).arg( layer.layerSize.x ).arg( layer.layerSize.y ) ); + xml.writeAttribute( "TileSize", QStringLiteral( "%1 x %2" ).arg( layer.tileSize.x ).arg( layer.tileSize.y ) ); + + auto writeStaticTile = [&xml, &writeProps](const tbin::Tile& tile) + { + if ( tile.props.size() == 0 ) + xml.writeEmptyElement( "Static" ); + else + xml.writeStartElement( "Static" ); + xml.writeAttribute( "Index", QString::number( tile.staticData.tileIndex ) ); + switch ( tile.staticData.blendMode ) + { + case 0: xml.writeAttribute( "BlendMode", "Alpha" ); break; + case 1: xml.writeAttribute( "BlendMode", "Additive" ); break; + default: throw std::invalid_argument( "Unsupported tile blend mode" ); + } + if ( tile.props.size() > 0 ) + { + writeProps( tile.props ); + xml.writeEndElement(); + } + }; + xml.writeStartElement( "TileArray" ); + std::string lastTilesheet; + for ( int iy = 0; iy < layer.layerSize.y; ++iy ) + { + xml.writeStartElement( "Row" ); + for ( int ix = 0; ix < layer.layerSize.x; ++ix ) + { + tbin::Tile tile = layer.tiles[ ix + iy * layer.layerSize.x ]; + if ( tile.tilesheet != "" && tile.tilesheet != lastTilesheet ) + { + xml.writeEmptyElement( "TileSheet" ); + xml.writeAttribute( "Ref", QString(tile.tilesheet.c_str()) ); + lastTilesheet = tile.tilesheet; + } + + if ( tile.isNullTile() ) + { + int nullCount = 1; + for ( ix += 1; ix < layer.layerSize.x; ++ix ) + { + if ( !layer.tiles[ ix + iy * layer.layerSize.x ].isNullTile() ) + { + --ix; + break; + } + nullCount += 1; + } + + xml.writeEmptyElement( "Null" ); + xml.writeAttribute( "Count", QString::number( nullCount ) ); + } + else if ( tile.staticData.tileIndex != -1 ) + { + writeStaticTile( tile ); + } + else if ( tile.animatedData.frames.size() > 0 ) + { + xml.writeStartElement( "Animated" ); + xml.writeAttribute( "Interval", QString::number( tile.animatedData.frameInterval ) ); + + xml.writeStartElement( "Frames" ); + std::string lastTilesheet; // Shadowing the previous declaration intentionally, because we explicitly do not want to use/affect it + for ( const auto& tile : tile.animatedData.frames ) + { + if ( tile.tilesheet != "" && tile.tilesheet != lastTilesheet ) + { + xml.writeEmptyElement( "TileSheet" ); + xml.writeAttribute( "Ref", QString(tile.tilesheet.c_str()) ); + lastTilesheet = tile.tilesheet; + } + writeStaticTile( tile ); + } + xml.writeEndElement(); + + if ( tile.props.size() > 0 ) + writeProps(tile.props); + + xml.writeEndElement(); + } + } + xml.writeEndElement(); + } + xml.writeEndElement(); + + writeProps( layer.props ); + + xml.writeEndElement(); + } + } + xml.writeEndElement(); + + writeProps(tmap.props); + } + xml.writeEndElement(); + xml.writeEndDocument(); + file.close(); + + return true; + } + catch (std::exception& e) + { + mError = tr("Exception: %1").arg(tr(e.what())); + return false; + } +} + +QString TideMapFormat::nameFilter() const +{ + return tr("tIDE map files (*.tide)"); +} + +QString TideMapFormat::shortName() const +{ + return QStringLiteral("tide"); +} + +bool TideMapFormat::supportsFile(const QString &fileName) const +{ + QFile file( fileName ); + if ( !file.open( QFile::ReadOnly | QFile::Text ) ) + return false; + + QXmlStreamReader xml(&file); + return xml.readNextStartElement() && xml.name() == QLatin1String("Map"); +} + +QString TideMapFormat::errorString() const +{ + return mError; +} + +} // namespace Tbin diff --git a/src/plugins/tbin/tidemapformat.h b/src/plugins/tbin/tidemapformat.h new file mode 100644 index 0000000000..cc7013ce00 --- /dev/null +++ b/src/plugins/tbin/tidemapformat.h @@ -0,0 +1,56 @@ +/* + * TBIN Tiled Plugin + * Copyright 2017, Casey Warrington + * + * This file is part of Tiled. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +#pragma once + +#include "tbin_global.h" + +#include "mapformat.h" + +#include + +namespace Tiled { +class Map; +} + +namespace Tbin { + +class TBINSHARED_EXPORT TideMapFormat : public Tiled::MapFormat +{ + Q_OBJECT + Q_INTERFACES(Tiled::MapFormat) + +public: + TideMapFormat(QObject *parent = nullptr); + + std::unique_ptr read(const QString &fileName) override; + bool supportsFile(const QString &fileName) const override; + + bool write(const Tiled::Map *map, const QString &fileName, Options options) override; + + QString nameFilter() const override; + QString shortName() const override; + QString errorString() const override; + +protected: + QString mError; +}; + +} // namespace Tbin From faa4b0bbe53478fec8d66fd492322b88f57511bb Mon Sep 17 00:00:00 2001 From: "Kanishka.." <143314565+kanishka0411@users.noreply.github.com> Date: Sat, 21 Feb 2026 00:12:32 +0530 Subject: [PATCH 2/3] Appstream screenshot captions (#4340) * Add descriptive captions to AppStream screenshots to fix pedantic warning * Update AppStream screenshot captions --- org.mapeditor.Tiled.appdata.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/org.mapeditor.Tiled.appdata.xml b/org.mapeditor.Tiled.appdata.xml index 3182c48487..76392ba358 100644 --- a/org.mapeditor.Tiled.appdata.xml +++ b/org.mapeditor.Tiled.appdata.xml @@ -379,10 +379,12 @@ https://www.mapeditor.org/img/appdata/screenshot_objects.png + Annotate your map with objects https://www.mapeditor.org/img/appdata/screenshot_terrain.png + Automate placement of terrain transitions From bd27cccaa136172c7e5d499468877360201265c2 Mon Sep 17 00:00:00 2001 From: Casey W Date: Fri, 20 Feb 2026 15:16:15 -0500 Subject: [PATCH 3/3] Fix copyright in tidemapformat (#4341) To match what I said it should be in PR #4308. --- src/plugins/tbin/tidemapformat.cpp | 2 +- src/plugins/tbin/tidemapformat.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/tbin/tidemapformat.cpp b/src/plugins/tbin/tidemapformat.cpp index b12b75d04c..44df5abb70 100644 --- a/src/plugins/tbin/tidemapformat.cpp +++ b/src/plugins/tbin/tidemapformat.cpp @@ -1,6 +1,6 @@ /* * TBIN Tiled Plugin - * Copyright 2017, Casey Warrington + * Copyright 2026, ConcernedApe LLC * * This file is part of Tiled. * diff --git a/src/plugins/tbin/tidemapformat.h b/src/plugins/tbin/tidemapformat.h index cc7013ce00..ba888a2984 100644 --- a/src/plugins/tbin/tidemapformat.h +++ b/src/plugins/tbin/tidemapformat.h @@ -1,6 +1,6 @@ /* * TBIN Tiled Plugin - * Copyright 2017, Casey Warrington + * Copyright 2026, ConcernedApe LLC * * This file is part of Tiled. *