Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions src/plugins/binmap/FORMAT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
Binary Map Format Specification
===============================

Simplest format possible. Consist of a 16 bytes long header and then data.
All numbers are little-endian, and except of the magic all are uint16_t's.
It does not store map properties, tileset specification, objects, etc.;
deliberately just the pure tile identifiers and nothing else.

Header
------

| Offset | Size | Description |
|-------:|-----:|---------------------------|
| 0 | 3 | magic bytes `MAP` |
| 3 | 1 | format version (always 0) |
| 4 | 2 | map width in tiles |
| 6 | 2 | map height in tiles |
| 8 | 2 | number of layers |
| 10 | 2 | tile width in pixels |
| 12 | 2 | tile height in pixels |
| 14 | 2 | orientation |

Orientation goes like: 0 = orthographic, 1 = isometric, 2 = hexagonal horizontal, 3 = hexagonal vertical.

Layer Data
----------

The header is followed by *number of layers* layer blocks, each *map height x map width x 2* bytes long.

Within each layer, first comes the top row, then the second row, etc. Within each row first the left most
column comes, then the second column, etc. Each cell value is a tile id stored as an unsigned short value
on 2 bytes (just the global tile id, without flags).
29 changes: 29 additions & 0 deletions src/plugins/binmap/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
Tiled BinMap Export Plugin
==========================

This is a [Tiled](https://mapeditor.org) plugin to dump tilemaps into a brainfuckingly simple binary format.

It ships BinMap as a plugin (which can be compiled into an .so file) as well as an extension (a .js file).

Installation
------------

You'll only need one of these, as they do exactly the same.

### JavaScript Extension

Copy `src/plugins/binmap/binmap-format.js` into `~/.config/tiled/extensions`.

### Tiled Plugin

1. Download the tiled source from [github](https://github.com/mapeditor/tiled).
2. Install all its dependencies (see its README for details).
3. Copy `src/plugins/binmap` directory into the downloaded repository.
4. Edit `src/plugins/plugins.qbs` and add `"binmap",` to the list.
5. In the repository's main directory, run `qbs`.

Documentation
-------------

The file format specification can be found [here](src/plugins/binmap/FORMAT.md). It is the simplest format possible,
just contains a fixed sized header and the uncompressed tile ids for each layer, that's all.
63 changes: 63 additions & 0 deletions src/plugins/binmap/binmap-format.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/// <reference types="@mapeditor/tiled-api" />

/*
* binmap-format.js
*
* This extension adds the 'binary map format' type to the Export As menu.
* This is an alternative to the .so plugin, written in JavaScript.
* To install, copy to `~/.config/tiled/extensions`
*
* Simplest format possible. Consist of a 16 bytes long header and then data.
* See FORMAT.md for details.
*/

tiled.registerMapFormat("binmap", {
name: "Binary map files",
extension: "map",

write: (map, fileName) => {
let numLayers = 0, gid = 1;
/* fix missing firstgid properties on tilesets */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a "fix". "First global IDs" simply don't exist outside of the file format. It's fine to assign them as dynamic properties like this, but you'll want to reword that comment. :-)

for (let i = 0; i < map.tilesets.length; ++i) {
map.tilesets[i].firstgid = gid;
gid += map.tilesets[i].tileCount;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll have to note that using tileCount unfortunately only works when there are no gaps in the tile IDs. Image collection tilesets can contain gaps, since when you remove tiles from such a tileset the other tiles don't change their ID.

If you want to support image collection tilesets, you'd currently need to iterate all tiles to find the maximum used tile ID.

}
/* count tile type layers */
for (let i = 0; i < map.layerCount; ++i) {
const layer = map.layerAt(i);
if (layer.isTileLayer) numLayers++;
}
/* file and buffers */
var file = new BinaryFile(fileName, BinaryFile.WriteOnly);
let header = new ArrayBuffer(16);
let item = new ArrayBuffer(2);
let hu16 = new Uint16Array(header);
let iu16 = new Uint16Array(item);
/* write header */
hu16[0] = 0x414D;
hu16[1] = 0x0050;
hu16[2] = map.width;
hu16[3] = map.height;
hu16[4] = numLayers;
hu16[5] = map.tileWidth;
hu16[6] = map.tileHeight;
/* looks like the Javascript API can't handle hexagonal maps */
hu16[7] = map.Isometric ? 1 : 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

map.Isometric doesn't exist. What you're looking for is map.orientation, which will beTileMap.Orthogonal, TileMap.Isometric, TileMap.Staggered or TileMap.Hexagonal.

file.write(header);
/* write layer tiles */
for (let i = 0; i < map.layerCount; ++i) {
const layer = map.layerAt(i);
if (!layer.isTileLayer) continue;

for (let y = 0; y < map.height; ++y) {
for (let x = 0; x < map.width; ++x) {
const tile = layer.tileAt(x, y);
/* convert local tile id to global tile id */
iu16[0] = tile == null ? 0 : tile.id + (tile.tileset ? tile.tileset.firstgid : 1);
file.write(item);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From a performance perspective I'd suggest writing entire layers at once rather than tile-by-tile.

}
}
}
file.commit();
},
});
41 changes: 41 additions & 0 deletions src/plugins/binmap/binmap.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* BinMap File Format
* Copyright 2025, bzt <bztsrc@gitlab>
*
* 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 <http://www.gnu.org/licenses/>.
*
* Simplest format possible. Consist of a 16 bytes long header and then data.
* See FORMAT.md for details.
*/

extern "C" {

#include <stdint.h>

typedef struct {
char magic[3]; /* 00 magic "MAP" */
uint8_t version; /* 03 format version */
uint16_t mapWidth; /* 04 map width */
uint16_t mapHeight; /* 06 map height */
uint16_t numLayers; /* 08 number of layers */
uint16_t tileWidth; /* 0A tile width */
uint16_t tileHeight; /* 0C tile height */
uint16_t orientation; /* 0E orientation (see O_* enums) */
} __attribute__((packed)) binmap_header_t;

enum { O_ORTHO, O_ISO, O_HEXH, O_HEXV };

/* Header followed by mapWidth * mapHeight * numLayers uint16_t and that's it. */

};
10 changes: 10 additions & 0 deletions src/plugins/binmap/binmap.qbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
TiledPlugin {
cpp.defines: base.concat(["BINMAP_LIBRARY"])

files: [
"binmap_global.h",
"binmapplugin.cpp",
"binmapplugin.h",
"plugin.json",
]
}
29 changes: 29 additions & 0 deletions src/plugins/binmap/binmap_global.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* BinMap Tiled Plugin
* Copyright 2025, bzt <bztsrc@gitlab>
*
* 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 <http://www.gnu.org/licenses/>.
*/

#pragma once

#include <QtCore/qglobal.h>

#if defined(BINMAP_LIBRARY)
# define BINMAPSHARED_EXPORT Q_DECL_EXPORT
#else
# define BINMAPSHARED_EXPORT Q_DECL_IMPORT
#endif
133 changes: 133 additions & 0 deletions src/plugins/binmap/binmapplugin.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* BinMap Tiled Plugin
* Copyright 2025, bzt <bztsrc@gitlab>
*
* 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 <http://www.gnu.org/licenses/>.
*
* To install, copy this directory under `tiled/src/plugins/binmap` directory,
* and add `"binmap",` line to `tiled/src/plugins/plugins.qbs` file.
*
* Simplest format possible. Consist of a 16 bytes long header and then data.
* See FORMAT.md for details.
*/

#include "binmapplugin.h"

#include "gidmapper.h"
#include "map.h"
#include "mapobject.h"
#include "savefile.h"
#include "tile.h"
#include "tiled.h"
#include "tilelayer.h"
#include "tileset.h"
#include "objectgroup.h"

#include <QCoreApplication>
#include <QDir>
#include <QFileInfo>
#include <QStringList>
#include <QStringView>
#include <QTextStream>

#include <memory>
#include <fstream>

#include "binmap.h"

using namespace Tiled;

namespace Binmap {

BinmapPlugin::BinmapPlugin()
{
}

QString BinmapPlugin::nameFilter() const
{
return tr("Binary map files (*.map)");
}

QString BinmapPlugin::shortName() const
{
return QStringLiteral("binmap");
}

QString BinmapPlugin::errorString() const
{
return mError;
}

bool BinmapPlugin::write(const Tiled::Map *map, const QString &fileName, Options options)
{
Q_UNUSED(options)
binmap_header_t header;

// construct header
memset( &header, 0, sizeof(header) );
header.magic[0] = 'M';
header.magic[1] = 'A';
header.magic[2] = 'P';
header.version = 0;
header.mapWidth = map->width();
header.mapHeight = map->height();
header.tileWidth = map->tileWidth();
header.tileHeight = map->tileHeight();
switch( map->orientation() ) {
case Map::Orthogonal: header.orientation = O_ORTHO; break;
case Map::Isometric: header.orientation = O_ISO; break;
case Map::Hexagonal: header.orientation = map->staggerAxis() == Map::StaggerX ? O_HEXH : O_HEXV; break;
default:
mError = QCoreApplication::translate("File Errors", "Unsupported map format.");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not support Map::Staggered in this file format? Seems like it'd just take one more possible orientation value. At least, since staggerIndex is not getting exported.

return false;
break;
}
// count layers
for (Layer *layer : map->layers()) {
if (layer->asTileLayer()) header.numLayers++;
}

// open file
std::ofstream file(fileName.toStdString(), std::ios::trunc | std::ios::binary);
if (!file) {
mError = QCoreApplication::translate("File Errors", "Could not open file for writing.");
return false;
}

GidMapper gidMapper(map->tilesets());

// write header
file.write( reinterpret_cast< const char* >( &header ), sizeof(header) );

// write layers
for (Layer *layer : map->layers()) {
if (TileLayer *tileLayer = layer->asTileLayer()) {
for (int y = 0; y < map->height(); ++y) {
for (int x = 0; x < map->width(); ++x) {
Cell t = tileLayer->cellAt(x, y);
unsigned short id = gidMapper.cellToGid(t);
file.write( reinterpret_cast< const char* >( &id ), sizeof(id) );
}
}
}
}

file.close();

return true;
}

} // namespace Flare
51 changes: 51 additions & 0 deletions src/plugins/binmap/binmapplugin.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* BinMap Tiled Plugin
* Copyright 2025, bzt <bztsrc@gitlab>
*
* 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 <http://www.gnu.org/licenses/>.
*/

#pragma once

#include "binmap_global.h"

#include "mapformat.h"

#include <QMap>
#include <QObject>
#include <QTextStream>

namespace Binmap {

class BINMAPSHARED_EXPORT BinmapPlugin : public Tiled::WritableMapFormat
{
Q_OBJECT
Q_INTERFACES(Tiled::MapFormat)
Q_PLUGIN_METADATA(IID "org.mapeditor.MapFormat" FILE "plugin.json")

public:
BinmapPlugin();

bool write(const Tiled::Map *map, const QString &fileName, Options options) override;
QString nameFilter() const override;
QString shortName() const override;
QString errorString() const override;

private:
QString mError;
};

} // namespace Binmap
1 change: 1 addition & 0 deletions src/plugins/binmap/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "defaultEnable": true }
Loading
Loading