Tiled XML/JSON import support

This commit is contained in:
John McCardle 2026-02-06 21:43:03 -05:00
commit b093e087e1
18 changed files with 3040 additions and 0 deletions

332
src/tiled/PyTileMapFile.cpp Normal file
View file

@ -0,0 +1,332 @@
#include "PyTileMapFile.h"
#include "PyTileSetFile.h"
#include "TiledParse.h"
#include "McRFPy_Doc.h"
#include "GridLayers.h"
#include <cstring>
using namespace mcrf::tiled;
// ============================================================
// Type lifecycle
// ============================================================
PyObject* PyTileMapFile::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds) {
auto* self = (PyTileMapFileObject*)type->tp_alloc(type, 0);
if (self) {
new (&self->data) std::shared_ptr<TileMapData>();
}
return (PyObject*)self;
}
int PyTileMapFile::init(PyTileMapFileObject* self, PyObject* args, PyObject* kwds) {
static const char* keywords[] = {"path", nullptr};
const char* path = nullptr;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "s", const_cast<char**>(keywords), &path))
return -1;
try {
self->data = loadTileMap(path);
} catch (const std::exception& e) {
PyErr_Format(PyExc_IOError, "Failed to load tilemap: %s", e.what());
return -1;
}
return 0;
}
void PyTileMapFile::dealloc(PyTileMapFileObject* self) {
self->data.~shared_ptr();
Py_TYPE(self)->tp_free((PyObject*)self);
}
PyObject* PyTileMapFile::repr(PyObject* obj) {
auto* self = (PyTileMapFileObject*)obj;
if (!self->data) {
return PyUnicode_FromString("<TileMapFile (uninitialized)>");
}
return PyUnicode_FromFormat("<TileMapFile %dx%d, %d tilesets, %d tile layers, %d object layers>",
self->data->width, self->data->height,
(int)self->data->tilesets.size(),
(int)self->data->tile_layers.size(),
(int)self->data->object_layers.size());
}
// ============================================================
// Properties
// ============================================================
PyObject* PyTileMapFile::get_width(PyTileMapFileObject* self, void*) {
return PyLong_FromLong(self->data->width);
}
PyObject* PyTileMapFile::get_height(PyTileMapFileObject* self, void*) {
return PyLong_FromLong(self->data->height);
}
PyObject* PyTileMapFile::get_tile_width(PyTileMapFileObject* self, void*) {
return PyLong_FromLong(self->data->tile_width);
}
PyObject* PyTileMapFile::get_tile_height(PyTileMapFileObject* self, void*) {
return PyLong_FromLong(self->data->tile_height);
}
PyObject* PyTileMapFile::get_orientation(PyTileMapFileObject* self, void*) {
return PyUnicode_FromString(self->data->orientation.c_str());
}
PyObject* PyTileMapFile::get_properties(PyTileMapFileObject* self, void*) {
return propertiesToPython(self->data->properties);
}
PyObject* PyTileMapFile::get_tileset_count(PyTileMapFileObject* self, void*) {
return PyLong_FromLong(self->data->tilesets.size());
}
PyObject* PyTileMapFile::get_tile_layer_names(PyTileMapFileObject* self, void*) {
PyObject* list = PyList_New(self->data->tile_layers.size());
if (!list) return NULL;
for (size_t i = 0; i < self->data->tile_layers.size(); i++) {
PyObject* name = PyUnicode_FromString(self->data->tile_layers[i].name.c_str());
if (!name) { Py_DECREF(list); return NULL; }
PyList_SET_ITEM(list, i, name);
}
return list;
}
PyObject* PyTileMapFile::get_object_layer_names(PyTileMapFileObject* self, void*) {
PyObject* list = PyList_New(self->data->object_layers.size());
if (!list) return NULL;
for (size_t i = 0; i < self->data->object_layers.size(); i++) {
PyObject* name = PyUnicode_FromString(self->data->object_layers[i].name.c_str());
if (!name) { Py_DECREF(list); return NULL; }
PyList_SET_ITEM(list, i, name);
}
return list;
}
// ============================================================
// Methods
// ============================================================
PyObject* PyTileMapFile::tileset(PyTileMapFileObject* self, PyObject* args) {
int index;
if (!PyArg_ParseTuple(args, "i", &index))
return NULL;
if (index < 0 || index >= (int)self->data->tilesets.size()) {
PyErr_Format(PyExc_IndexError, "Tileset index %d out of range (0..%d)",
index, (int)self->data->tilesets.size() - 1);
return NULL;
}
const auto& ref = self->data->tilesets[index];
// Create a TileSetFile wrapping the existing parsed data
auto* ts_type = &mcrfpydef::PyTileSetFileType;
auto* ts = (PyTileSetFileObject*)ts_type->tp_alloc(ts_type, 0);
if (!ts) return NULL;
new (&ts->data) std::shared_ptr<TileSetData>(ref.tileset);
// Return (firstgid, TileSetFile)
PyObject* result = Py_BuildValue("(iN)", ref.firstgid, (PyObject*)ts);
return result;
}
PyObject* PyTileMapFile::tile_layer_data(PyTileMapFileObject* self, PyObject* args) {
const char* name;
if (!PyArg_ParseTuple(args, "s", &name))
return NULL;
for (const auto& tl : self->data->tile_layers) {
if (tl.name == name) {
PyObject* list = PyList_New(tl.global_gids.size());
if (!list) return NULL;
for (size_t i = 0; i < tl.global_gids.size(); i++) {
PyList_SET_ITEM(list, i, PyLong_FromUnsignedLong(tl.global_gids[i]));
}
return list;
}
}
PyErr_Format(PyExc_KeyError, "No tile layer named '%s'", name);
return NULL;
}
PyObject* PyTileMapFile::resolve_gid(PyTileMapFileObject* self, PyObject* args) {
unsigned int gid;
if (!PyArg_ParseTuple(args, "I", &gid))
return NULL;
if (gid == 0) {
// GID 0 = empty tile
return Py_BuildValue("(ii)", -1, -1);
}
// Strip flip flags (top 3 bits of a 32-bit GID)
uint32_t clean_gid = gid & 0x1FFFFFFF;
// Find which tileset this GID belongs to (tilesets sorted by firstgid)
int ts_index = -1;
for (int i = (int)self->data->tilesets.size() - 1; i >= 0; i--) {
if (clean_gid >= (uint32_t)self->data->tilesets[i].firstgid) {
ts_index = i;
break;
}
}
if (ts_index < 0) {
return Py_BuildValue("(ii)", -1, -1);
}
int local_id = clean_gid - self->data->tilesets[ts_index].firstgid;
return Py_BuildValue("(ii)", ts_index, local_id);
}
PyObject* PyTileMapFile::object_layer(PyTileMapFileObject* self, PyObject* args) {
const char* name;
if (!PyArg_ParseTuple(args, "s", &name))
return NULL;
for (const auto& ol : self->data->object_layers) {
if (ol.name == name) {
return jsonToPython(ol.objects);
}
}
PyErr_Format(PyExc_KeyError, "No object layer named '%s'", name);
return NULL;
}
PyObject* PyTileMapFile::apply_to_tile_layer(PyTileMapFileObject* self, PyObject* args, PyObject* kwds) {
static const char* keywords[] = {"tile_layer", "layer_name", "tileset_index", nullptr};
PyObject* tlayer_obj;
const char* layer_name;
int tileset_index = 0;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "Os|i", const_cast<char**>(keywords),
&tlayer_obj, &layer_name, &tileset_index))
return NULL;
// Validate TileLayer
// Check type by name since PyTileLayerType is static-per-TU
const char* type_name = Py_TYPE(tlayer_obj)->tp_name;
if (!type_name || strcmp(type_name, "mcrfpy.TileLayer") != 0) {
PyErr_SetString(PyExc_TypeError, "First argument must be a TileLayer");
return NULL;
}
// Find the tile layer data
const TileLayerData* tld = nullptr;
for (const auto& tl : self->data->tile_layers) {
if (tl.name == layer_name) {
tld = &tl;
break;
}
}
if (!tld) {
PyErr_Format(PyExc_KeyError, "No tile layer named '%s'", layer_name);
return NULL;
}
if (tileset_index < 0 || tileset_index >= (int)self->data->tilesets.size()) {
PyErr_Format(PyExc_IndexError, "Tileset index %d out of range", tileset_index);
return NULL;
}
int firstgid = self->data->tilesets[tileset_index].firstgid;
auto* tlayer = (PyTileLayerObject*)tlayer_obj;
int w = tld->width;
int h = tld->height;
for (int y = 0; y < h && y < tlayer->data->grid_y; y++) {
for (int x = 0; x < w && x < tlayer->data->grid_x; x++) {
uint32_t gid = tld->global_gids[y * w + x];
if (gid == 0) {
tlayer->data->at(x, y) = -1; // empty
continue;
}
uint32_t clean_gid = gid & 0x1FFFFFFF;
int local_id = static_cast<int>(clean_gid) - firstgid;
if (local_id >= 0) {
tlayer->data->at(x, y) = local_id;
}
}
}
tlayer->data->markDirty();
Py_RETURN_NONE;
}
// ============================================================
// Method/GetSet tables
// ============================================================
PyMethodDef PyTileMapFile::methods[] = {
{"tileset", (PyCFunction)PyTileMapFile::tileset, METH_VARARGS,
MCRF_METHOD(TileMapFile, tileset,
MCRF_SIG("(index: int)", "tuple[int, TileSetFile]"),
MCRF_DESC("Get a referenced tileset by index."),
MCRF_ARGS_START
MCRF_ARG("index", "Tileset index (0-based)")
MCRF_RETURNS("Tuple of (firstgid, TileSetFile).")
)},
{"tile_layer_data", (PyCFunction)PyTileMapFile::tile_layer_data, METH_VARARGS,
MCRF_METHOD(TileMapFile, tile_layer_data,
MCRF_SIG("(name: str)", "list[int]"),
MCRF_DESC("Get raw global GID data for a tile layer."),
MCRF_ARGS_START
MCRF_ARG("name", "Name of the tile layer")
MCRF_RETURNS("Flat list of global GIDs (0 = empty tile).")
MCRF_RAISES("KeyError", "If no tile layer with that name exists")
)},
{"resolve_gid", (PyCFunction)PyTileMapFile::resolve_gid, METH_VARARGS,
MCRF_METHOD(TileMapFile, resolve_gid,
MCRF_SIG("(gid: int)", "tuple[int, int]"),
MCRF_DESC("Resolve a global tile ID to tileset index and local tile ID."),
MCRF_ARGS_START
MCRF_ARG("gid", "Global tile ID from tile_layer_data()")
MCRF_RETURNS("Tuple of (tileset_index, local_tile_id). (-1, -1) for empty/invalid.")
)},
{"object_layer", (PyCFunction)PyTileMapFile::object_layer, METH_VARARGS,
MCRF_METHOD(TileMapFile, object_layer,
MCRF_SIG("(name: str)", "list[dict]"),
MCRF_DESC("Get objects from an object layer as Python dicts."),
MCRF_ARGS_START
MCRF_ARG("name", "Name of the object layer")
MCRF_RETURNS("List of dicts with object properties (id, name, x, y, width, height, etc.).")
MCRF_RAISES("KeyError", "If no object layer with that name exists")
)},
{"apply_to_tile_layer", (PyCFunction)PyTileMapFile::apply_to_tile_layer, METH_VARARGS | METH_KEYWORDS,
MCRF_METHOD(TileMapFile, apply_to_tile_layer,
MCRF_SIG("(tile_layer: TileLayer, layer_name: str, tileset_index: int = 0)", "None"),
MCRF_DESC("Resolve GIDs and write sprite indices into a TileLayer."),
MCRF_ARGS_START
MCRF_ARG("tile_layer", "Target TileLayer to write into")
MCRF_ARG("layer_name", "Name of the tile layer in this map")
MCRF_ARG("tileset_index", "Which tileset to resolve GIDs against (default 0)")
)},
{NULL}
};
PyGetSetDef PyTileMapFile::getsetters[] = {
{"width", (getter)PyTileMapFile::get_width, NULL,
MCRF_PROPERTY(width, "Map width in tiles (int, read-only)."), NULL},
{"height", (getter)PyTileMapFile::get_height, NULL,
MCRF_PROPERTY(height, "Map height in tiles (int, read-only)."), NULL},
{"tile_width", (getter)PyTileMapFile::get_tile_width, NULL,
MCRF_PROPERTY(tile_width, "Tile width in pixels (int, read-only)."), NULL},
{"tile_height", (getter)PyTileMapFile::get_tile_height, NULL,
MCRF_PROPERTY(tile_height, "Tile height in pixels (int, read-only)."), NULL},
{"orientation", (getter)PyTileMapFile::get_orientation, NULL,
MCRF_PROPERTY(orientation, "Map orientation, e.g. 'orthogonal' (str, read-only)."), NULL},
{"properties", (getter)PyTileMapFile::get_properties, NULL,
MCRF_PROPERTY(properties, "Custom map properties as a dict (read-only)."), NULL},
{"tileset_count", (getter)PyTileMapFile::get_tileset_count, NULL,
MCRF_PROPERTY(tileset_count, "Number of referenced tilesets (int, read-only)."), NULL},
{"tile_layer_names", (getter)PyTileMapFile::get_tile_layer_names, NULL,
MCRF_PROPERTY(tile_layer_names, "List of tile layer names (read-only)."), NULL},
{"object_layer_names", (getter)PyTileMapFile::get_object_layer_names, NULL,
MCRF_PROPERTY(object_layer_names, "List of object layer names (read-only)."), NULL},
{NULL}
};

81
src/tiled/PyTileMapFile.h Normal file
View file

@ -0,0 +1,81 @@
#pragma once
#include "Python.h"
#include "TiledTypes.h"
#include <memory>
// Python object structure
typedef struct PyTileMapFileObject {
PyObject_HEAD
std::shared_ptr<mcrf::tiled::TileMapData> data;
} PyTileMapFileObject;
// Python binding class
class PyTileMapFile {
public:
static PyObject* pynew(PyTypeObject* type, PyObject* args, PyObject* kwds);
static int init(PyTileMapFileObject* self, PyObject* args, PyObject* kwds);
static void dealloc(PyTileMapFileObject* self);
static PyObject* repr(PyObject* obj);
// Read-only properties
static PyObject* get_width(PyTileMapFileObject* self, void* closure);
static PyObject* get_height(PyTileMapFileObject* self, void* closure);
static PyObject* get_tile_width(PyTileMapFileObject* self, void* closure);
static PyObject* get_tile_height(PyTileMapFileObject* self, void* closure);
static PyObject* get_orientation(PyTileMapFileObject* self, void* closure);
static PyObject* get_properties(PyTileMapFileObject* self, void* closure);
static PyObject* get_tileset_count(PyTileMapFileObject* self, void* closure);
static PyObject* get_tile_layer_names(PyTileMapFileObject* self, void* closure);
static PyObject* get_object_layer_names(PyTileMapFileObject* self, void* closure);
// Methods
static PyObject* tileset(PyTileMapFileObject* self, PyObject* args);
static PyObject* tile_layer_data(PyTileMapFileObject* self, PyObject* args);
static PyObject* resolve_gid(PyTileMapFileObject* self, PyObject* args);
static PyObject* object_layer(PyTileMapFileObject* self, PyObject* args);
static PyObject* apply_to_tile_layer(PyTileMapFileObject* self, PyObject* args, PyObject* kwds);
static PyMethodDef methods[];
static PyGetSetDef getsetters[];
};
// Type definition in mcrfpydef namespace
namespace mcrfpydef {
inline PyTypeObject PyTileMapFileType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.TileMapFile",
.tp_basicsize = sizeof(PyTileMapFileObject),
.tp_itemsize = 0,
.tp_dealloc = (destructor)PyTileMapFile::dealloc,
.tp_repr = PyTileMapFile::repr,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR(
"TileMapFile(path: str)\n\n"
"Load a Tiled map file (.tmx or .tmj).\n\n"
"Parses the map and its referenced tilesets, providing access to tile layers,\n"
"object layers, and GID resolution.\n\n"
"Args:\n"
" path: Path to the .tmx or .tmj map file.\n\n"
"Properties:\n"
" width (int, read-only): Map width in tiles.\n"
" height (int, read-only): Map height in tiles.\n"
" tile_width (int, read-only): Tile width in pixels.\n"
" tile_height (int, read-only): Tile height in pixels.\n"
" orientation (str, read-only): Map orientation (e.g. 'orthogonal').\n"
" properties (dict, read-only): Custom map properties.\n"
" tileset_count (int, read-only): Number of referenced tilesets.\n"
" tile_layer_names (list, read-only): Names of tile layers.\n"
" object_layer_names (list, read-only): Names of object layers.\n\n"
"Example:\n"
" tm = mcrfpy.TileMapFile('map.tmx')\n"
" data = tm.tile_layer_data('Ground')\n"
" tm.apply_to_tile_layer(my_tile_layer, 'Ground')\n"
),
.tp_methods = nullptr, // Set before PyType_Ready
.tp_getset = nullptr, // Set before PyType_Ready
.tp_init = (initproc)PyTileMapFile::init,
.tp_new = PyTileMapFile::pynew,
};
} // namespace mcrfpydef

234
src/tiled/PyTileSetFile.cpp Normal file
View file

@ -0,0 +1,234 @@
#include "PyTileSetFile.h"
#include "TiledParse.h"
#include "PyWangSet.h"
#include "McRFPy_Doc.h"
using namespace mcrf::tiled;
// ============================================================
// Type lifecycle
// ============================================================
PyObject* PyTileSetFile::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds) {
auto* self = (PyTileSetFileObject*)type->tp_alloc(type, 0);
if (self) {
new (&self->data) std::shared_ptr<TileSetData>();
}
return (PyObject*)self;
}
int PyTileSetFile::init(PyTileSetFileObject* self, PyObject* args, PyObject* kwds) {
static const char* keywords[] = {"path", nullptr};
const char* path = nullptr;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "s", const_cast<char**>(keywords), &path))
return -1;
try {
self->data = loadTileSet(path);
} catch (const std::exception& e) {
PyErr_Format(PyExc_IOError, "Failed to load tileset: %s", e.what());
return -1;
}
return 0;
}
void PyTileSetFile::dealloc(PyTileSetFileObject* self) {
self->data.~shared_ptr();
Py_TYPE(self)->tp_free((PyObject*)self);
}
PyObject* PyTileSetFile::repr(PyObject* obj) {
auto* self = (PyTileSetFileObject*)obj;
if (!self->data) {
return PyUnicode_FromString("<TileSetFile (uninitialized)>");
}
return PyUnicode_FromFormat("<TileSetFile '%s' (%d tiles, %dx%d)>",
self->data->name.c_str(), self->data->tile_count,
self->data->tile_width, self->data->tile_height);
}
// ============================================================
// Properties (all read-only)
// ============================================================
PyObject* PyTileSetFile::get_name(PyTileSetFileObject* self, void*) {
return PyUnicode_FromString(self->data->name.c_str());
}
PyObject* PyTileSetFile::get_tile_width(PyTileSetFileObject* self, void*) {
return PyLong_FromLong(self->data->tile_width);
}
PyObject* PyTileSetFile::get_tile_height(PyTileSetFileObject* self, void*) {
return PyLong_FromLong(self->data->tile_height);
}
PyObject* PyTileSetFile::get_tile_count(PyTileSetFileObject* self, void*) {
return PyLong_FromLong(self->data->tile_count);
}
PyObject* PyTileSetFile::get_columns(PyTileSetFileObject* self, void*) {
return PyLong_FromLong(self->data->columns);
}
PyObject* PyTileSetFile::get_margin(PyTileSetFileObject* self, void*) {
return PyLong_FromLong(self->data->margin);
}
PyObject* PyTileSetFile::get_spacing(PyTileSetFileObject* self, void*) {
return PyLong_FromLong(self->data->spacing);
}
PyObject* PyTileSetFile::get_image_source(PyTileSetFileObject* self, void*) {
return PyUnicode_FromString(self->data->image_source.c_str());
}
PyObject* PyTileSetFile::get_properties(PyTileSetFileObject* self, void*) {
return propertiesToPython(self->data->properties);
}
PyObject* PyTileSetFile::get_wang_sets(PyTileSetFileObject* self, void*) {
PyObject* list = PyList_New(self->data->wang_sets.size());
if (!list) return NULL;
for (size_t i = 0; i < self->data->wang_sets.size(); i++) {
PyObject* ws = PyWangSet::create(self->data, static_cast<int>(i));
if (!ws) {
Py_DECREF(list);
return NULL;
}
PyList_SET_ITEM(list, i, ws);
}
return list;
}
// ============================================================
// Methods
// ============================================================
PyObject* PyTileSetFile::to_texture(PyTileSetFileObject* self, PyObject* args) {
// Create a PyTexture using the image source and tile dimensions
// Get the Texture type from the mcrfpy module (safe cross-compilation-unit access)
PyObject* mcrfpy_module = PyImport_ImportModule("mcrfpy");
if (!mcrfpy_module) return NULL;
PyObject* tex_type = PyObject_GetAttrString(mcrfpy_module, "Texture");
Py_DECREF(mcrfpy_module);
if (!tex_type) return NULL;
PyObject* tex_args = Py_BuildValue("(sii)",
self->data->image_source.c_str(),
self->data->tile_width,
self->data->tile_height);
if (!tex_args) { Py_DECREF(tex_type); return NULL; }
PyObject* tex = PyObject_Call(tex_type, tex_args, NULL);
Py_DECREF(tex_type);
Py_DECREF(tex_args);
return tex;
}
PyObject* PyTileSetFile::tile_info(PyTileSetFileObject* self, PyObject* args) {
int tile_id;
if (!PyArg_ParseTuple(args, "i", &tile_id))
return NULL;
auto it = self->data->tile_info.find(tile_id);
if (it == self->data->tile_info.end()) {
Py_RETURN_NONE;
}
const TileInfo& ti = it->second;
PyObject* dict = PyDict_New();
if (!dict) return NULL;
// Properties
PyObject* props = propertiesToPython(ti.properties);
if (!props) { Py_DECREF(dict); return NULL; }
PyDict_SetItemString(dict, "properties", props);
Py_DECREF(props);
// Animation
PyObject* anim_list = PyList_New(ti.animation.size());
if (!anim_list) { Py_DECREF(dict); return NULL; }
for (size_t i = 0; i < ti.animation.size(); i++) {
PyObject* frame = Py_BuildValue("(ii)", ti.animation[i].tile_id, ti.animation[i].duration_ms);
if (!frame) { Py_DECREF(anim_list); Py_DECREF(dict); return NULL; }
PyList_SET_ITEM(anim_list, i, frame);
}
PyDict_SetItemString(dict, "animation", anim_list);
Py_DECREF(anim_list);
return dict;
}
PyObject* PyTileSetFile::wang_set(PyTileSetFileObject* self, PyObject* args) {
const char* name;
if (!PyArg_ParseTuple(args, "s", &name))
return NULL;
for (size_t i = 0; i < self->data->wang_sets.size(); i++) {
if (self->data->wang_sets[i].name == name) {
return PyWangSet::create(self->data, static_cast<int>(i));
}
}
PyErr_Format(PyExc_KeyError, "No WangSet named '%s'", name);
return NULL;
}
// ============================================================
// Method/GetSet tables
// ============================================================
PyMethodDef PyTileSetFile::methods[] = {
{"to_texture", (PyCFunction)PyTileSetFile::to_texture, METH_NOARGS,
MCRF_METHOD(TileSetFile, to_texture,
MCRF_SIG("()", "Texture"),
MCRF_DESC("Create a Texture from the tileset image."),
MCRF_RETURNS("A Texture object for use with TileLayer.")
)},
{"tile_info", (PyCFunction)PyTileSetFile::tile_info, METH_VARARGS,
MCRF_METHOD(TileSetFile, tile_info,
MCRF_SIG("(tile_id: int)", "dict | None"),
MCRF_DESC("Get metadata for a specific tile."),
MCRF_ARGS_START
MCRF_ARG("tile_id", "Local tile ID (0-based)")
MCRF_RETURNS("Dict with 'properties' and 'animation' keys, or None if no metadata.")
)},
{"wang_set", (PyCFunction)PyTileSetFile::wang_set, METH_VARARGS,
MCRF_METHOD(TileSetFile, wang_set,
MCRF_SIG("(name: str)", "WangSet"),
MCRF_DESC("Look up a WangSet by name."),
MCRF_ARGS_START
MCRF_ARG("name", "Name of the Wang set")
MCRF_RETURNS("The WangSet object.")
MCRF_RAISES("KeyError", "If no WangSet with that name exists")
)},
{NULL}
};
PyGetSetDef PyTileSetFile::getsetters[] = {
{"name", (getter)PyTileSetFile::get_name, NULL,
MCRF_PROPERTY(name, "Tileset name (str, read-only)."), NULL},
{"tile_width", (getter)PyTileSetFile::get_tile_width, NULL,
MCRF_PROPERTY(tile_width, "Width of each tile in pixels (int, read-only)."), NULL},
{"tile_height", (getter)PyTileSetFile::get_tile_height, NULL,
MCRF_PROPERTY(tile_height, "Height of each tile in pixels (int, read-only)."), NULL},
{"tile_count", (getter)PyTileSetFile::get_tile_count, NULL,
MCRF_PROPERTY(tile_count, "Total number of tiles (int, read-only)."), NULL},
{"columns", (getter)PyTileSetFile::get_columns, NULL,
MCRF_PROPERTY(columns, "Number of columns in tileset image (int, read-only)."), NULL},
{"margin", (getter)PyTileSetFile::get_margin, NULL,
MCRF_PROPERTY(margin, "Margin around tiles in pixels (int, read-only)."), NULL},
{"spacing", (getter)PyTileSetFile::get_spacing, NULL,
MCRF_PROPERTY(spacing, "Spacing between tiles in pixels (int, read-only)."), NULL},
{"image_source", (getter)PyTileSetFile::get_image_source, NULL,
MCRF_PROPERTY(image_source, "Resolved path to the tileset image file (str, read-only)."), NULL},
{"properties", (getter)PyTileSetFile::get_properties, NULL,
MCRF_PROPERTY(properties, "Custom tileset properties as a dict (read-only)."), NULL},
{"wang_sets", (getter)PyTileSetFile::get_wang_sets, NULL,
MCRF_PROPERTY(wang_sets, "List of WangSet objects from this tileset (read-only)."), NULL},
{NULL}
};

79
src/tiled/PyTileSetFile.h Normal file
View file

@ -0,0 +1,79 @@
#pragma once
#include "Python.h"
#include "TiledTypes.h"
#include <memory>
// Python object structure
typedef struct PyTileSetFileObject {
PyObject_HEAD
std::shared_ptr<mcrf::tiled::TileSetData> data;
} PyTileSetFileObject;
// Python binding class
class PyTileSetFile {
public:
static PyObject* pynew(PyTypeObject* type, PyObject* args, PyObject* kwds);
static int init(PyTileSetFileObject* self, PyObject* args, PyObject* kwds);
static void dealloc(PyTileSetFileObject* self);
static PyObject* repr(PyObject* obj);
// Read-only properties
static PyObject* get_name(PyTileSetFileObject* self, void* closure);
static PyObject* get_tile_width(PyTileSetFileObject* self, void* closure);
static PyObject* get_tile_height(PyTileSetFileObject* self, void* closure);
static PyObject* get_tile_count(PyTileSetFileObject* self, void* closure);
static PyObject* get_columns(PyTileSetFileObject* self, void* closure);
static PyObject* get_margin(PyTileSetFileObject* self, void* closure);
static PyObject* get_spacing(PyTileSetFileObject* self, void* closure);
static PyObject* get_image_source(PyTileSetFileObject* self, void* closure);
static PyObject* get_properties(PyTileSetFileObject* self, void* closure);
static PyObject* get_wang_sets(PyTileSetFileObject* self, void* closure);
// Methods
static PyObject* to_texture(PyTileSetFileObject* self, PyObject* args);
static PyObject* tile_info(PyTileSetFileObject* self, PyObject* args);
static PyObject* wang_set(PyTileSetFileObject* self, PyObject* args);
static PyMethodDef methods[];
static PyGetSetDef getsetters[];
};
// Type definition in mcrfpydef namespace
namespace mcrfpydef {
inline PyTypeObject PyTileSetFileType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.TileSetFile",
.tp_basicsize = sizeof(PyTileSetFileObject),
.tp_itemsize = 0,
.tp_dealloc = (destructor)PyTileSetFile::dealloc,
.tp_repr = PyTileSetFile::repr,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR(
"TileSetFile(path: str)\n\n"
"Load a Tiled tileset file (.tsx or .tsj).\n\n"
"Parses the tileset and provides access to tile metadata, properties,\n"
"Wang sets, and texture creation.\n\n"
"Args:\n"
" path: Path to the .tsx or .tsj tileset file.\n\n"
"Properties:\n"
" name (str, read-only): Tileset name.\n"
" tile_width (int, read-only): Width of each tile in pixels.\n"
" tile_height (int, read-only): Height of each tile in pixels.\n"
" tile_count (int, read-only): Total number of tiles.\n"
" columns (int, read-only): Number of columns in the tileset image.\n"
" image_source (str, read-only): Resolved path to the tileset image.\n"
" properties (dict, read-only): Custom properties from the tileset.\n"
" wang_sets (list, read-only): List of WangSet objects.\n\n"
"Example:\n"
" ts = mcrfpy.TileSetFile('tileset.tsx')\n"
" texture = ts.to_texture()\n"
" print(f'{ts.name}: {ts.tile_count} tiles')\n"
),
.tp_methods = nullptr, // Set before PyType_Ready
.tp_getset = nullptr, // Set before PyType_Ready
.tp_init = (initproc)PyTileSetFile::init,
.tp_new = PyTileSetFile::pynew,
};
} // namespace mcrfpydef

266
src/tiled/PyWangSet.cpp Normal file
View file

@ -0,0 +1,266 @@
#include "PyWangSet.h"
#include "TiledParse.h"
#include "WangResolve.h"
#include "McRFPy_Doc.h"
#include "PyDiscreteMap.h"
#include "GridLayers.h"
#include <cstring>
using namespace mcrf::tiled;
// ============================================================
// Helper
// ============================================================
const WangSet& PyWangSet::getWangSet(PyWangSetObject* self) {
return self->parent->wang_sets[self->wang_set_index];
}
// ============================================================
// Factory
// ============================================================
PyObject* PyWangSet::create(std::shared_ptr<TileSetData> parent, int index) {
auto* type = &mcrfpydef::PyWangSetType;
auto* self = (PyWangSetObject*)type->tp_alloc(type, 0);
if (!self) return NULL;
new (&self->parent) std::shared_ptr<TileSetData>(parent);
self->wang_set_index = index;
return (PyObject*)self;
}
// ============================================================
// Type lifecycle
// ============================================================
void PyWangSet::dealloc(PyWangSetObject* self) {
self->parent.~shared_ptr();
Py_TYPE(self)->tp_free((PyObject*)self);
}
PyObject* PyWangSet::repr(PyObject* obj) {
auto* self = (PyWangSetObject*)obj;
const auto& ws = getWangSet(self);
const char* type_str = "unknown";
switch (ws.type) {
case WangSetType::Corner: type_str = "corner"; break;
case WangSetType::Edge: type_str = "edge"; break;
case WangSetType::Mixed: type_str = "mixed"; break;
}
return PyUnicode_FromFormat("<WangSet '%s' type='%s' colors=%d>",
ws.name.c_str(), type_str, (int)ws.colors.size());
}
// ============================================================
// Properties
// ============================================================
PyObject* PyWangSet::get_name(PyWangSetObject* self, void*) {
return PyUnicode_FromString(getWangSet(self).name.c_str());
}
PyObject* PyWangSet::get_type(PyWangSetObject* self, void*) {
switch (getWangSet(self).type) {
case WangSetType::Corner: return PyUnicode_FromString("corner");
case WangSetType::Edge: return PyUnicode_FromString("edge");
case WangSetType::Mixed: return PyUnicode_FromString("mixed");
}
return PyUnicode_FromString("unknown");
}
PyObject* PyWangSet::get_color_count(PyWangSetObject* self, void*) {
return PyLong_FromLong(getWangSet(self).colors.size());
}
PyObject* PyWangSet::get_colors(PyWangSetObject* self, void*) {
const auto& ws = getWangSet(self);
PyObject* list = PyList_New(ws.colors.size());
if (!list) return NULL;
for (size_t i = 0; i < ws.colors.size(); i++) {
const auto& wc = ws.colors[i];
PyObject* dict = Py_BuildValue("{s:s, s:i, s:i, s:f}",
"name", wc.name.c_str(),
"index", wc.index,
"tile_id", wc.tile_id,
"probability", (double)wc.probability);
if (!dict) {
Py_DECREF(list);
return NULL;
}
PyList_SET_ITEM(list, i, dict);
}
return list;
}
// ============================================================
// Methods
// ============================================================
// Convert a name like "Grass Terrain" to "GRASS_TERRAIN"
static std::string toUpperSnakeCase(const std::string& s) {
std::string result;
result.reserve(s.size());
for (size_t i = 0; i < s.size(); i++) {
char c = s[i];
if (c == ' ' || c == '-') {
result += '_';
} else {
result += static_cast<char>(toupper(static_cast<unsigned char>(c)));
}
}
return result;
}
PyObject* PyWangSet::terrain_enum(PyWangSetObject* self, PyObject*) {
const auto& ws = getWangSet(self);
// Import IntEnum from enum module
PyObject* enum_module = PyImport_ImportModule("enum");
if (!enum_module) return NULL;
PyObject* int_enum = PyObject_GetAttrString(enum_module, "IntEnum");
Py_DECREF(enum_module);
if (!int_enum) return NULL;
// Build members dict: NONE=0, then each color
PyObject* members = PyDict_New();
if (!members) { Py_DECREF(int_enum); return NULL; }
// NONE = 0 (unset terrain)
PyObject* zero = PyLong_FromLong(0);
PyDict_SetItemString(members, "NONE", zero);
Py_DECREF(zero);
for (const auto& wc : ws.colors) {
std::string key = toUpperSnakeCase(wc.name);
PyObject* val = PyLong_FromLong(wc.index);
PyDict_SetItemString(members, key.c_str(), val);
Py_DECREF(val);
}
// Create enum class: IntEnum(ws.name, members)
PyObject* name = PyUnicode_FromString(ws.name.c_str());
PyObject* args = PyTuple_Pack(2, name, members);
Py_DECREF(name);
Py_DECREF(members);
PyObject* enum_class = PyObject_Call(int_enum, args, NULL);
Py_DECREF(args);
Py_DECREF(int_enum);
return enum_class;
}
PyObject* PyWangSet::resolve(PyWangSetObject* self, PyObject* args) {
PyObject* dmap_obj;
if (!PyArg_ParseTuple(args, "O", &dmap_obj))
return NULL;
// Check type by name since static types differ per translation unit
const char* dmap_type_name = Py_TYPE(dmap_obj)->tp_name;
if (!dmap_type_name || strcmp(dmap_type_name, "mcrfpy.DiscreteMap") != 0) {
PyErr_SetString(PyExc_TypeError, "Expected a DiscreteMap object");
return NULL;
}
auto* dmap = (PyDiscreteMapObject*)dmap_obj;
const auto& ws = getWangSet(self);
std::vector<int> result = resolveWangTerrain(dmap->values, dmap->w, dmap->h, ws);
// Convert to Python list
PyObject* list = PyList_New(result.size());
if (!list) return NULL;
for (size_t i = 0; i < result.size(); i++) {
PyList_SET_ITEM(list, i, PyLong_FromLong(result[i]));
}
return list;
}
PyObject* PyWangSet::apply(PyWangSetObject* self, PyObject* args, PyObject* kwds) {
static const char* keywords[] = {"discrete_map", "tile_layer", nullptr};
PyObject* dmap_obj;
PyObject* tlayer_obj;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO", const_cast<char**>(keywords),
&dmap_obj, &tlayer_obj))
return NULL;
// Validate DiscreteMap (check by name since static types differ per TU)
const char* dmap_tn = Py_TYPE(dmap_obj)->tp_name;
if (!dmap_tn || strcmp(dmap_tn, "mcrfpy.DiscreteMap") != 0) {
PyErr_SetString(PyExc_TypeError, "First argument must be a DiscreteMap");
return NULL;
}
// Validate TileLayer
const char* tl_tn = Py_TYPE(tlayer_obj)->tp_name;
if (!tl_tn || strcmp(tl_tn, "mcrfpy.TileLayer") != 0) {
PyErr_SetString(PyExc_TypeError, "Second argument must be a TileLayer");
return NULL;
}
auto* dmap = (PyDiscreteMapObject*)dmap_obj;
auto* tlayer = (PyTileLayerObject*)tlayer_obj;
const auto& ws = getWangSet(self);
// Resolve terrain to tile indices
std::vector<int> tile_ids = resolveWangTerrain(dmap->values, dmap->w, dmap->h, ws);
// Write into TileLayer
int w = dmap->w;
int h = dmap->h;
for (int y = 0; y < h && y < tlayer->data->grid_y; y++) {
for (int x = 0; x < w && x < tlayer->data->grid_x; x++) {
int tid = tile_ids[y * w + x];
if (tid >= 0) {
tlayer->data->at(x, y) = tid;
}
}
}
tlayer->data->markDirty();
Py_RETURN_NONE;
}
// ============================================================
// Method/GetSet tables
// ============================================================
PyMethodDef PyWangSet::methods[] = {
{"terrain_enum", (PyCFunction)PyWangSet::terrain_enum, METH_NOARGS,
MCRF_METHOD(WangSet, terrain_enum,
MCRF_SIG("()", "IntEnum"),
MCRF_DESC("Generate a Python IntEnum from this WangSet's terrain colors."),
MCRF_RETURNS("IntEnum class with NONE=0 and one member per color (UPPER_SNAKE_CASE).")
)},
{"resolve", (PyCFunction)PyWangSet::resolve, METH_VARARGS,
MCRF_METHOD(WangSet, resolve,
MCRF_SIG("(discrete_map: DiscreteMap)", "list[int]"),
MCRF_DESC("Resolve terrain data to tile indices using Wang tile rules."),
MCRF_ARGS_START
MCRF_ARG("discrete_map", "A DiscreteMap with terrain IDs matching this WangSet's colors")
MCRF_RETURNS("List of tile IDs (one per cell). -1 means no matching Wang tile.")
)},
{"apply", (PyCFunction)PyWangSet::apply, METH_VARARGS | METH_KEYWORDS,
MCRF_METHOD(WangSet, apply,
MCRF_SIG("(discrete_map: DiscreteMap, tile_layer: TileLayer)", "None"),
MCRF_DESC("Resolve terrain and write tile indices directly into a TileLayer."),
MCRF_ARGS_START
MCRF_ARG("discrete_map", "A DiscreteMap with terrain IDs")
MCRF_ARG("tile_layer", "Target TileLayer to write resolved tiles into")
)},
{NULL}
};
PyGetSetDef PyWangSet::getsetters[] = {
{"name", (getter)PyWangSet::get_name, NULL,
MCRF_PROPERTY(name, "Wang set name (str, read-only)."), NULL},
{"type", (getter)PyWangSet::get_type, NULL,
MCRF_PROPERTY(type, "Wang set type: 'corner', 'edge', or 'mixed' (str, read-only)."), NULL},
{"color_count", (getter)PyWangSet::get_color_count, NULL,
MCRF_PROPERTY(color_count, "Number of terrain colors (int, read-only)."), NULL},
{"colors", (getter)PyWangSet::get_colors, NULL,
MCRF_PROPERTY(colors, "List of color dicts with name, index, tile_id, probability (read-only)."), NULL},
{NULL}
};

72
src/tiled/PyWangSet.h Normal file
View file

@ -0,0 +1,72 @@
#pragma once
#include "Python.h"
#include "TiledTypes.h"
#include <memory>
// Python object structure
// Holds a shared_ptr to the parent TileSetData (keeps it alive) + index into wang_sets
typedef struct PyWangSetObject {
PyObject_HEAD
std::shared_ptr<mcrf::tiled::TileSetData> parent;
int wang_set_index;
} PyWangSetObject;
// Python binding class
class PyWangSet {
public:
// Factory: create a PyWangSet from parent tileset + index
static PyObject* create(std::shared_ptr<mcrf::tiled::TileSetData> parent, int index);
static void dealloc(PyWangSetObject* self);
static PyObject* repr(PyObject* obj);
// Read-only properties
static PyObject* get_name(PyWangSetObject* self, void* closure);
static PyObject* get_type(PyWangSetObject* self, void* closure);
static PyObject* get_color_count(PyWangSetObject* self, void* closure);
static PyObject* get_colors(PyWangSetObject* self, void* closure);
// Methods
static PyObject* terrain_enum(PyWangSetObject* self, PyObject* args);
static PyObject* resolve(PyWangSetObject* self, PyObject* args);
static PyObject* apply(PyWangSetObject* self, PyObject* args, PyObject* kwds);
static PyMethodDef methods[];
static PyGetSetDef getsetters[];
private:
// Helper: get the WangSet reference
static const mcrf::tiled::WangSet& getWangSet(PyWangSetObject* self);
};
// Type definition in mcrfpydef namespace
namespace mcrfpydef {
inline PyTypeObject PyWangSetType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.WangSet",
.tp_basicsize = sizeof(PyWangSetObject),
.tp_itemsize = 0,
.tp_dealloc = (destructor)PyWangSet::dealloc,
.tp_repr = PyWangSet::repr,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR(
"WangSet - Wang terrain auto-tile set from a Tiled tileset.\n\n"
"WangSets are obtained from TileSetFile.wang_sets or TileSetFile.wang_set().\n"
"They map abstract terrain types to concrete sprite indices using Tiled's\n"
"Wang tile algorithm.\n\n"
"Properties:\n"
" name (str, read-only): Wang set name.\n"
" type (str, read-only): 'corner', 'edge', or 'mixed'.\n"
" color_count (int, read-only): Number of terrain colors.\n"
" colors (list, read-only): List of color dicts.\n\n"
"Example:\n"
" ws = tileset.wang_set('overworld')\n"
" Terrain = ws.terrain_enum()\n"
" tiles = ws.resolve(discrete_map)\n"
),
.tp_methods = nullptr, // Set before PyType_Ready
.tp_getset = nullptr, // Set before PyType_Ready
};
} // namespace mcrfpydef

772
src/tiled/TiledParse.cpp Normal file
View file

@ -0,0 +1,772 @@
#include "TiledParse.h"
#include "RapidXML/rapidxml.hpp"
#include <fstream>
#include <sstream>
#include <stdexcept>
#include <algorithm>
#include <filesystem>
namespace mcrf {
namespace tiled {
// ============================================================
// Utility helpers
// ============================================================
static std::string readFile(const std::string& path) {
std::ifstream f(path);
if (!f.is_open()) {
throw std::runtime_error("Cannot open file: " + path);
}
std::stringstream ss;
ss << f.rdbuf();
return ss.str();
}
static std::string parentDir(const std::string& path) {
std::filesystem::path p(path);
return p.parent_path().string();
}
static std::string resolvePath(const std::string& base_dir, const std::string& relative) {
std::filesystem::path p = std::filesystem::path(base_dir) / relative;
return p.lexically_normal().string();
}
static bool endsWith(const std::string& str, const std::string& suffix) {
if (suffix.size() > str.size()) return false;
return str.compare(str.size() - suffix.size(), suffix.size(), suffix) == 0;
}
// Get attribute value or empty string
static std::string xmlAttr(rapidxml::xml_node<>* node, const char* name) {
auto* attr = node->first_attribute(name);
return attr ? std::string(attr->value(), attr->value_size()) : "";
}
static int xmlAttrInt(rapidxml::xml_node<>* node, const char* name, int def = 0) {
auto* attr = node->first_attribute(name);
if (!attr) return def;
return std::stoi(std::string(attr->value(), attr->value_size()));
}
static float xmlAttrFloat(rapidxml::xml_node<>* node, const char* name, float def = 0.0f) {
auto* attr = node->first_attribute(name);
if (!attr) return def;
return std::stof(std::string(attr->value(), attr->value_size()));
}
// ============================================================
// Property conversion (Raw → Final)
// ============================================================
static PropertyValue convertProperty(const RawProperty& raw) {
if (raw.type == "bool") {
return PropertyValue(raw.value == "true");
} else if (raw.type == "int") {
return PropertyValue(std::stoi(raw.value));
} else if (raw.type == "float") {
return PropertyValue(std::stof(raw.value));
} else {
// Default: string (includes empty type)
return PropertyValue(raw.value);
}
}
static std::unordered_map<std::string, PropertyValue> convertProperties(
const std::vector<RawProperty>& raw_props) {
std::unordered_map<std::string, PropertyValue> result;
for (const auto& rp : raw_props) {
result[rp.name] = convertProperty(rp);
}
return result;
}
// ============================================================
// WangSet packing
// ============================================================
uint64_t WangSet::packWangId(const std::array<int, 8>& id) {
// Pack 8 values (each 0-255) into 64-bit integer
// Each value gets 8 bits
uint64_t packed = 0;
for (int i = 0; i < 8; i++) {
packed |= (static_cast<uint64_t>(id[i] & 0xFF)) << (i * 8);
}
return packed;
}
// ============================================================
// XML property parsing (shared by TSX and TMX)
// ============================================================
static void parseXmlProperties(rapidxml::xml_node<>* parent, std::vector<RawProperty>& out) {
auto* props_node = parent->first_node("properties");
if (!props_node) return;
for (auto* prop = props_node->first_node("property"); prop; prop = prop->next_sibling("property")) {
RawProperty rp;
rp.name = xmlAttr(prop, "name");
rp.type = xmlAttr(prop, "type");
rp.value = xmlAttr(prop, "value");
// Some properties have value as node text instead of attribute
if (rp.value.empty() && prop->value_size() > 0) {
rp.value = std::string(prop->value(), prop->value_size());
}
out.push_back(std::move(rp));
}
}
// ============================================================
// TSX parser (XML tileset)
// ============================================================
static RawTileSet parseTSX(const std::string& path) {
std::string text = readFile(path);
rapidxml::xml_document<> doc;
doc.parse<0>(text.data());
auto* tileset_node = doc.first_node("tileset");
if (!tileset_node) {
throw std::runtime_error("No <tileset> element in: " + path);
}
RawTileSet raw;
raw.name = xmlAttr(tileset_node, "name");
raw.tile_width = xmlAttrInt(tileset_node, "tilewidth");
raw.tile_height = xmlAttrInt(tileset_node, "tileheight");
raw.tile_count = xmlAttrInt(tileset_node, "tilecount");
raw.columns = xmlAttrInt(tileset_node, "columns");
raw.margin = xmlAttrInt(tileset_node, "margin");
raw.spacing = xmlAttrInt(tileset_node, "spacing");
// Image element
auto* image_node = tileset_node->first_node("image");
if (image_node) {
raw.image_source = xmlAttr(image_node, "source");
raw.image_width = xmlAttrInt(image_node, "width");
raw.image_height = xmlAttrInt(image_node, "height");
}
// Properties
parseXmlProperties(tileset_node, raw.properties);
// Tile elements (for per-tile properties and animations)
for (auto* tile = tileset_node->first_node("tile"); tile; tile = tile->next_sibling("tile")) {
RawTile rt;
rt.id = xmlAttrInt(tile, "id");
parseXmlProperties(tile, rt.properties);
// Animation frames
auto* anim = tile->first_node("animation");
if (anim) {
for (auto* frame = anim->first_node("frame"); frame; frame = frame->next_sibling("frame")) {
int tid = xmlAttrInt(frame, "tileid");
int dur = xmlAttrInt(frame, "duration");
rt.animation_frames.emplace_back(tid, dur);
}
}
raw.tiles.push_back(std::move(rt));
}
// Wang sets
auto* wangsets_node = tileset_node->first_node("wangsets");
if (wangsets_node) {
for (auto* ws = wangsets_node->first_node("wangset"); ws; ws = ws->next_sibling("wangset")) {
RawWangSet rws;
rws.name = xmlAttr(ws, "name");
rws.type = xmlAttr(ws, "type");
// Wang colors (1-indexed by position in list)
int color_idx = 1;
for (auto* wc = ws->first_node("wangcolor"); wc; wc = wc->next_sibling("wangcolor")) {
RawWangColor rwc;
rwc.name = xmlAttr(wc, "name");
rwc.color_index = color_idx++;
rwc.tile_id = xmlAttrInt(wc, "tile");
rwc.probability = xmlAttrFloat(wc, "probability", 1.0f);
rws.colors.push_back(std::move(rwc));
}
// Wang tiles
for (auto* wt = ws->first_node("wangtile"); wt; wt = wt->next_sibling("wangtile")) {
RawWangTile rwt;
rwt.tile_id = xmlAttrInt(wt, "tileid");
// Parse wangid: comma-separated 8 integers
std::string wid_str = xmlAttr(wt, "wangid");
std::array<int, 8> wid = {};
std::istringstream iss(wid_str);
std::string token;
int idx = 0;
while (std::getline(iss, token, ',') && idx < 8) {
wid[idx++] = std::stoi(token);
}
rwt.wang_id = wid;
rws.tiles.push_back(std::move(rwt));
}
raw.wang_sets.push_back(std::move(rws));
}
}
return raw;
}
// ============================================================
// TSJ parser (JSON tileset)
// ============================================================
static void parseJsonProperties(const nlohmann::json& j, std::vector<RawProperty>& out) {
if (!j.contains("properties") || !j["properties"].is_array()) return;
for (const auto& prop : j["properties"]) {
RawProperty rp;
rp.name = prop.value("name", "");
rp.type = prop.value("type", "");
// Value can be different JSON types
if (prop.contains("value")) {
const auto& val = prop["value"];
if (val.is_boolean()) {
rp.type = "bool";
rp.value = val.get<bool>() ? "true" : "false";
} else if (val.is_number_integer()) {
rp.type = "int";
rp.value = std::to_string(val.get<int>());
} else if (val.is_number_float()) {
rp.type = "float";
rp.value = std::to_string(val.get<float>());
} else if (val.is_string()) {
rp.value = val.get<std::string>();
}
}
out.push_back(std::move(rp));
}
}
static RawTileSet parseTSJ(const std::string& path) {
std::string text = readFile(path);
nlohmann::json j = nlohmann::json::parse(text);
RawTileSet raw;
raw.name = j.value("name", "");
raw.tile_width = j.value("tilewidth", 0);
raw.tile_height = j.value("tileheight", 0);
raw.tile_count = j.value("tilecount", 0);
raw.columns = j.value("columns", 0);
raw.margin = j.value("margin", 0);
raw.spacing = j.value("spacing", 0);
raw.image_source = j.value("image", "");
raw.image_width = j.value("imagewidth", 0);
raw.image_height = j.value("imageheight", 0);
parseJsonProperties(j, raw.properties);
// Tiles
if (j.contains("tiles") && j["tiles"].is_array()) {
for (const auto& tile : j["tiles"]) {
RawTile rt;
rt.id = tile.value("id", 0);
parseJsonProperties(tile, rt.properties);
if (tile.contains("animation") && tile["animation"].is_array()) {
for (const auto& frame : tile["animation"]) {
int tid = frame.value("tileid", 0);
int dur = frame.value("duration", 0);
rt.animation_frames.emplace_back(tid, dur);
}
}
raw.tiles.push_back(std::move(rt));
}
}
// Wang sets
if (j.contains("wangsets") && j["wangsets"].is_array()) {
for (const auto& ws : j["wangsets"]) {
RawWangSet rws;
rws.name = ws.value("name", "");
rws.type = ws.value("type", "");
if (ws.contains("colors") && ws["colors"].is_array()) {
int ci = 1; // Tiled wang colors are 1-indexed
for (const auto& wc : ws["colors"]) {
RawWangColor rwc;
rwc.name = wc.value("name", "");
rwc.color_index = ci++;
rwc.tile_id = wc.value("tile", -1);
rwc.probability = wc.value("probability", 1.0f);
rws.colors.push_back(std::move(rwc));
}
}
if (ws.contains("wangtiles") && ws["wangtiles"].is_array()) {
for (const auto& wt : ws["wangtiles"]) {
RawWangTile rwt;
rwt.tile_id = wt.value("tileid", 0);
std::array<int, 8> wid = {};
if (wt.contains("wangid") && wt["wangid"].is_array()) {
for (int i = 0; i < 8 && i < (int)wt["wangid"].size(); i++) {
wid[i] = wt["wangid"][i].get<int>();
}
}
rwt.wang_id = wid;
rws.tiles.push_back(std::move(rwt));
}
}
raw.wang_sets.push_back(std::move(rws));
}
}
return raw;
}
// ============================================================
// Builder: RawTileSet → TileSetData
// ============================================================
static std::shared_ptr<TileSetData> buildTileSet(const RawTileSet& raw, const std::string& source_path) {
auto ts = std::make_shared<TileSetData>();
ts->source_path = source_path;
ts->name = raw.name;
ts->tile_width = raw.tile_width;
ts->tile_height = raw.tile_height;
ts->tile_count = raw.tile_count;
ts->columns = raw.columns;
ts->margin = raw.margin;
ts->spacing = raw.spacing;
ts->image_width = raw.image_width;
ts->image_height = raw.image_height;
// Resolve image path relative to tileset file
std::string base_dir = parentDir(source_path);
ts->image_source = resolvePath(base_dir, raw.image_source);
// Convert properties
ts->properties = convertProperties(raw.properties);
// Convert tile info
for (const auto& rt : raw.tiles) {
TileInfo ti;
ti.id = rt.id;
ti.properties = convertProperties(rt.properties);
for (const auto& [tid, dur] : rt.animation_frames) {
ti.animation.push_back({tid, dur});
}
ts->tile_info[ti.id] = std::move(ti);
}
// Convert wang sets
for (const auto& rws : raw.wang_sets) {
WangSet ws;
ws.name = rws.name;
if (rws.type == "corner") ws.type = WangSetType::Corner;
else if (rws.type == "edge") ws.type = WangSetType::Edge;
else ws.type = WangSetType::Mixed;
for (const auto& rwc : rws.colors) {
WangColor wc;
wc.name = rwc.name;
wc.index = rwc.color_index;
wc.tile_id = rwc.tile_id;
wc.probability = rwc.probability;
ws.colors.push_back(std::move(wc));
}
// Build lookup table
for (const auto& rwt : rws.tiles) {
uint64_t key = WangSet::packWangId(rwt.wang_id);
ws.wang_lookup[key] = rwt.tile_id;
}
ts->wang_sets.push_back(std::move(ws));
}
return ts;
}
// ============================================================
// TMX parser (XML tilemap)
// ============================================================
static RawTileMap parseTMX(const std::string& path) {
std::string text = readFile(path);
rapidxml::xml_document<> doc;
doc.parse<0>(text.data());
auto* map_node = doc.first_node("map");
if (!map_node) {
throw std::runtime_error("No <map> element in: " + path);
}
RawTileMap raw;
raw.width = xmlAttrInt(map_node, "width");
raw.height = xmlAttrInt(map_node, "height");
raw.tile_width = xmlAttrInt(map_node, "tilewidth");
raw.tile_height = xmlAttrInt(map_node, "tileheight");
raw.orientation = xmlAttr(map_node, "orientation");
parseXmlProperties(map_node, raw.properties);
// Tileset references
for (auto* ts = map_node->first_node("tileset"); ts; ts = ts->next_sibling("tileset")) {
RawTileSetRef ref;
ref.firstgid = xmlAttrInt(ts, "firstgid");
ref.source = xmlAttr(ts, "source");
raw.tileset_refs.push_back(std::move(ref));
}
// Layers
for (auto* child = map_node->first_node(); child; child = child->next_sibling()) {
std::string node_name(child->name(), child->name_size());
if (node_name == "layer") {
RawLayer layer;
layer.name = xmlAttr(child, "name");
layer.type = "tilelayer";
layer.width = xmlAttrInt(child, "width");
layer.height = xmlAttrInt(child, "height");
std::string vis = xmlAttr(child, "visible");
layer.visible = vis.empty() || vis != "0";
layer.opacity = xmlAttrFloat(child, "opacity", 1.0f);
parseXmlProperties(child, layer.properties);
// Parse CSV tile data
auto* data_node = child->first_node("data");
if (data_node) {
std::string encoding = xmlAttr(data_node, "encoding");
if (!encoding.empty() && encoding != "csv") {
throw std::runtime_error("Unsupported tile data encoding: " + encoding +
" (only CSV supported). File: " + path);
}
std::string csv(data_node->value(), data_node->value_size());
std::istringstream iss(csv);
std::string token;
while (std::getline(iss, token, ',')) {
// Trim whitespace
auto start = token.find_first_not_of(" \t\r\n");
if (start == std::string::npos) continue;
auto end = token.find_last_not_of(" \t\r\n");
token = token.substr(start, end - start + 1);
if (!token.empty()) {
layer.tile_data.push_back(static_cast<uint32_t>(std::stoul(token)));
}
}
}
raw.layers.push_back(std::move(layer));
}
else if (node_name == "objectgroup") {
RawLayer layer;
layer.name = xmlAttr(child, "name");
layer.type = "objectgroup";
std::string vis = xmlAttr(child, "visible");
layer.visible = vis.empty() || vis != "0";
layer.opacity = xmlAttrFloat(child, "opacity", 1.0f);
parseXmlProperties(child, layer.properties);
// Convert XML objects to JSON for uniform Python interface
nlohmann::json objects_arr = nlohmann::json::array();
for (auto* obj = child->first_node("object"); obj; obj = obj->next_sibling("object")) {
nlohmann::json obj_json;
std::string id_str = xmlAttr(obj, "id");
if (!id_str.empty()) obj_json["id"] = std::stoi(id_str);
std::string name = xmlAttr(obj, "name");
if (!name.empty()) obj_json["name"] = name;
std::string type = xmlAttr(obj, "type");
if (!type.empty()) obj_json["type"] = type;
std::string x_str = xmlAttr(obj, "x");
if (!x_str.empty()) obj_json["x"] = std::stof(x_str);
std::string y_str = xmlAttr(obj, "y");
if (!y_str.empty()) obj_json["y"] = std::stof(y_str);
std::string w_str = xmlAttr(obj, "width");
if (!w_str.empty()) obj_json["width"] = std::stof(w_str);
std::string h_str = xmlAttr(obj, "height");
if (!h_str.empty()) obj_json["height"] = std::stof(h_str);
std::string rot_str = xmlAttr(obj, "rotation");
if (!rot_str.empty()) obj_json["rotation"] = std::stof(rot_str);
std::string visible_str = xmlAttr(obj, "visible");
if (!visible_str.empty()) obj_json["visible"] = (visible_str != "0");
// Object properties
std::vector<RawProperty> obj_props;
parseXmlProperties(obj, obj_props);
if (!obj_props.empty()) {
nlohmann::json props_json;
for (const auto& rp : obj_props) {
if (rp.type == "bool") props_json[rp.name] = (rp.value == "true");
else if (rp.type == "int") props_json[rp.name] = std::stoi(rp.value);
else if (rp.type == "float") props_json[rp.name] = std::stof(rp.value);
else props_json[rp.name] = rp.value;
}
obj_json["properties"] = props_json;
}
// Check for point/ellipse/polygon sub-elements
if (obj->first_node("point")) {
obj_json["point"] = true;
}
if (obj->first_node("ellipse")) {
obj_json["ellipse"] = true;
}
auto* polygon_node = obj->first_node("polygon");
if (polygon_node) {
std::string points_str = xmlAttr(polygon_node, "points");
nlohmann::json points_arr = nlohmann::json::array();
std::istringstream pss(points_str);
std::string pt;
while (pss >> pt) {
auto comma = pt.find(',');
if (comma != std::string::npos) {
nlohmann::json point;
point["x"] = std::stof(pt.substr(0, comma));
point["y"] = std::stof(pt.substr(comma + 1));
points_arr.push_back(point);
}
}
obj_json["polygon"] = points_arr;
}
objects_arr.push_back(std::move(obj_json));
}
layer.objects_json = objects_arr;
raw.layers.push_back(std::move(layer));
}
}
return raw;
}
// ============================================================
// TMJ parser (JSON tilemap)
// ============================================================
static RawTileMap parseTMJ(const std::string& path) {
std::string text = readFile(path);
nlohmann::json j = nlohmann::json::parse(text);
RawTileMap raw;
raw.width = j.value("width", 0);
raw.height = j.value("height", 0);
raw.tile_width = j.value("tilewidth", 0);
raw.tile_height = j.value("tileheight", 0);
raw.orientation = j.value("orientation", "orthogonal");
parseJsonProperties(j, raw.properties);
// Tileset references
if (j.contains("tilesets") && j["tilesets"].is_array()) {
for (const auto& ts : j["tilesets"]) {
RawTileSetRef ref;
ref.firstgid = ts.value("firstgid", 0);
ref.source = ts.value("source", "");
raw.tileset_refs.push_back(std::move(ref));
}
}
// Layers
if (j.contains("layers") && j["layers"].is_array()) {
for (const auto& layer_json : j["layers"]) {
RawLayer layer;
layer.name = layer_json.value("name", "");
layer.type = layer_json.value("type", "");
layer.width = layer_json.value("width", 0);
layer.height = layer_json.value("height", 0);
layer.visible = layer_json.value("visible", true);
layer.opacity = layer_json.value("opacity", 1.0f);
parseJsonProperties(layer_json, layer.properties);
if (layer.type == "tilelayer") {
if (layer_json.contains("data") && layer_json["data"].is_array()) {
for (const auto& val : layer_json["data"]) {
layer.tile_data.push_back(val.get<uint32_t>());
}
}
}
else if (layer.type == "objectgroup") {
if (layer_json.contains("objects")) {
layer.objects_json = layer_json["objects"];
}
}
raw.layers.push_back(std::move(layer));
}
}
return raw;
}
// ============================================================
// Builder: RawTileMap → TileMapData
// ============================================================
static std::shared_ptr<TileMapData> buildTileMap(const RawTileMap& raw, const std::string& source_path) {
auto tm = std::make_shared<TileMapData>();
tm->source_path = source_path;
tm->width = raw.width;
tm->height = raw.height;
tm->tile_width = raw.tile_width;
tm->tile_height = raw.tile_height;
tm->orientation = raw.orientation;
tm->properties = convertProperties(raw.properties);
// Load referenced tilesets
std::string base_dir = parentDir(source_path);
for (const auto& ref : raw.tileset_refs) {
TileMapData::TileSetRef ts_ref;
ts_ref.firstgid = ref.firstgid;
std::string ts_path = resolvePath(base_dir, ref.source);
ts_ref.tileset = loadTileSet(ts_path);
tm->tilesets.push_back(std::move(ts_ref));
}
// Separate tile layers from object layers
for (const auto& rl : raw.layers) {
if (rl.type == "tilelayer") {
TileLayerData tld;
tld.name = rl.name;
tld.width = rl.width;
tld.height = rl.height;
tld.visible = rl.visible;
tld.opacity = rl.opacity;
tld.global_gids = rl.tile_data;
tm->tile_layers.push_back(std::move(tld));
}
else if (rl.type == "objectgroup") {
ObjectLayerData old;
old.name = rl.name;
old.visible = rl.visible;
old.opacity = rl.opacity;
old.objects = rl.objects_json;
old.properties = convertProperties(rl.properties);
tm->object_layers.push_back(std::move(old));
}
}
return tm;
}
// ============================================================
// Public API: auto-detect and load
// ============================================================
std::shared_ptr<TileSetData> loadTileSet(const std::string& path) {
std::string abs_path = std::filesystem::absolute(path).string();
RawTileSet raw;
if (endsWith(abs_path, ".tsx")) {
raw = parseTSX(abs_path);
} else if (endsWith(abs_path, ".tsj") || endsWith(abs_path, ".json")) {
raw = parseTSJ(abs_path);
} else {
throw std::runtime_error("Unknown tileset format (expected .tsx or .tsj): " + path);
}
return buildTileSet(raw, abs_path);
}
std::shared_ptr<TileMapData> loadTileMap(const std::string& path) {
std::string abs_path = std::filesystem::absolute(path).string();
RawTileMap raw;
if (endsWith(abs_path, ".tmx")) {
raw = parseTMX(abs_path);
} else if (endsWith(abs_path, ".tmj") || endsWith(abs_path, ".json")) {
raw = parseTMJ(abs_path);
} else {
throw std::runtime_error("Unknown tilemap format (expected .tmx or .tmj): " + path);
}
return buildTileMap(raw, abs_path);
}
// ============================================================
// JSON → Python conversion (for object layers)
// ============================================================
PyObject* jsonToPython(const nlohmann::json& j) {
if (j.is_null()) {
Py_RETURN_NONE;
}
if (j.is_boolean()) {
return PyBool_FromLong(j.get<bool>());
}
if (j.is_number_integer()) {
return PyLong_FromLongLong(j.get<long long>());
}
if (j.is_number_float()) {
return PyFloat_FromDouble(j.get<double>());
}
if (j.is_string()) {
const std::string& s = j.get_ref<const std::string&>();
return PyUnicode_FromStringAndSize(s.c_str(), s.size());
}
if (j.is_array()) {
PyObject* list = PyList_New(j.size());
if (!list) return NULL;
for (size_t i = 0; i < j.size(); i++) {
PyObject* item = jsonToPython(j[i]);
if (!item) {
Py_DECREF(list);
return NULL;
}
PyList_SET_ITEM(list, i, item); // steals ref
}
return list;
}
if (j.is_object()) {
PyObject* dict = PyDict_New();
if (!dict) return NULL;
for (auto it = j.begin(); it != j.end(); ++it) {
PyObject* val = jsonToPython(it.value());
if (!val) {
Py_DECREF(dict);
return NULL;
}
if (PyDict_SetItemString(dict, it.key().c_str(), val) < 0) {
Py_DECREF(val);
Py_DECREF(dict);
return NULL;
}
Py_DECREF(val);
}
return dict;
}
Py_RETURN_NONE;
}
// ============================================================
// PropertyValue → Python conversion
// ============================================================
PyObject* propertyValueToPython(const PropertyValue& val) {
return std::visit([](auto&& arg) -> PyObject* {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, bool>) {
return PyBool_FromLong(arg);
} else if constexpr (std::is_same_v<T, int>) {
return PyLong_FromLong(arg);
} else if constexpr (std::is_same_v<T, float>) {
return PyFloat_FromDouble(arg);
} else if constexpr (std::is_same_v<T, std::string>) {
return PyUnicode_FromStringAndSize(arg.c_str(), arg.size());
}
Py_RETURN_NONE;
}, val);
}
PyObject* propertiesToPython(const std::unordered_map<std::string, PropertyValue>& props) {
PyObject* dict = PyDict_New();
if (!dict) return NULL;
for (const auto& [key, val] : props) {
PyObject* py_val = propertyValueToPython(val);
if (!py_val) {
Py_DECREF(dict);
return NULL;
}
if (PyDict_SetItemString(dict, key.c_str(), py_val) < 0) {
Py_DECREF(py_val);
Py_DECREF(dict);
return NULL;
}
Py_DECREF(py_val);
}
return dict;
}
} // namespace tiled
} // namespace mcrf

24
src/tiled/TiledParse.h Normal file
View file

@ -0,0 +1,24 @@
#pragma once
#include "TiledTypes.h"
#include <Python.h>
namespace mcrf {
namespace tiled {
// Load a tileset from .tsx or .tsj (auto-detect by extension)
std::shared_ptr<TileSetData> loadTileSet(const std::string& path);
// Load a tilemap from .tmx or .tmj (auto-detect by extension)
std::shared_ptr<TileMapData> loadTileMap(const std::string& path);
// Convert nlohmann::json to Python object (for object layers)
PyObject* jsonToPython(const nlohmann::json& j);
// Convert PropertyValue to Python object
PyObject* propertyValueToPython(const PropertyValue& val);
// Convert a properties map to Python dict
PyObject* propertiesToPython(const std::unordered_map<std::string, PropertyValue>& props);
} // namespace tiled
} // namespace mcrf

186
src/tiled/TiledTypes.h Normal file
View file

@ -0,0 +1,186 @@
#pragma once
#include <string>
#include <vector>
#include <array>
#include <unordered_map>
#include <variant>
#include <memory>
#include <cstdint>
#include <nlohmann/json.hpp>
namespace mcrf {
namespace tiled {
// ============================================================
// Raw intermediate structs — populated by thin XML/JSON parsers
// ============================================================
struct RawProperty {
std::string name;
std::string type; // "bool", "int", "float", "string" (or empty = string)
std::string value;
};
struct RawTile {
int id;
std::vector<RawProperty> properties;
std::vector<std::pair<int, int>> animation_frames; // (tile_id, duration_ms)
};
struct RawWangColor {
std::string name;
int color_index;
int tile_id;
float probability;
};
struct RawWangTile {
int tile_id;
std::array<int, 8> wang_id;
};
struct RawWangSet {
std::string name;
std::string type; // "corner", "edge", "mixed"
std::vector<RawWangColor> colors;
std::vector<RawWangTile> tiles;
};
struct RawTileSet {
std::string name;
std::string image_source;
int tile_width = 0;
int tile_height = 0;
int tile_count = 0;
int columns = 0;
int margin = 0;
int spacing = 0;
int image_width = 0;
int image_height = 0;
std::vector<RawProperty> properties;
std::vector<RawTile> tiles;
std::vector<RawWangSet> wang_sets;
};
struct RawTileSetRef {
int firstgid;
std::string source;
};
struct RawLayer {
std::string name;
std::string type; // "tilelayer", "objectgroup"
int width = 0;
int height = 0;
bool visible = true;
float opacity = 1.0f;
std::vector<RawProperty> properties;
std::vector<uint32_t> tile_data;
nlohmann::json objects_json;
};
struct RawTileMap {
int width = 0;
int height = 0;
int tile_width = 0;
int tile_height = 0;
std::string orientation; // "orthogonal", etc.
std::vector<RawProperty> properties;
std::vector<RawTileSetRef> tileset_refs;
std::vector<RawLayer> layers;
};
// ============================================================
// Final (built) types — what Python bindings expose
// ============================================================
using PropertyValue = std::variant<bool, int, float, std::string>;
struct KeyFrame {
int tile_id;
int duration_ms;
};
struct TileInfo {
int id;
std::unordered_map<std::string, PropertyValue> properties;
std::vector<KeyFrame> animation;
};
enum class WangSetType {
Corner,
Edge,
Mixed
};
struct WangColor {
std::string name;
int index;
int tile_id;
float probability;
};
struct WangSet {
std::string name;
WangSetType type;
std::vector<WangColor> colors;
// Maps packed wang_id → tile_id for O(1) lookup
std::unordered_map<uint64_t, int> wang_lookup;
static uint64_t packWangId(const std::array<int, 8>& id);
};
struct TileSetData {
std::string name;
std::string source_path; // Filesystem path of the .tsx/.tsj file
std::string image_source; // Resolved path to image file
int tile_width = 0;
int tile_height = 0;
int tile_count = 0;
int columns = 0;
int margin = 0;
int spacing = 0;
int image_width = 0;
int image_height = 0;
std::unordered_map<std::string, PropertyValue> properties;
std::unordered_map<int, TileInfo> tile_info;
std::vector<WangSet> wang_sets;
};
struct TileLayerData {
std::string name;
int width = 0;
int height = 0;
bool visible = true;
float opacity = 1.0f;
std::vector<uint32_t> global_gids;
};
struct ObjectLayerData {
std::string name;
bool visible = true;
float opacity = 1.0f;
nlohmann::json objects;
std::unordered_map<std::string, PropertyValue> properties;
};
struct TileMapData {
std::string source_path; // Filesystem path of the .tmx/.tmj file
int width = 0;
int height = 0;
int tile_width = 0;
int tile_height = 0;
std::string orientation;
std::unordered_map<std::string, PropertyValue> properties;
struct TileSetRef {
int firstgid;
std::shared_ptr<TileSetData> tileset;
};
std::vector<TileSetRef> tilesets;
std::vector<TileLayerData> tile_layers;
std::vector<ObjectLayerData> object_layers;
};
} // namespace tiled
} // namespace mcrf

142
src/tiled/WangResolve.cpp Normal file
View file

@ -0,0 +1,142 @@
#include "WangResolve.h"
#include <array>
namespace mcrf {
namespace tiled {
// Helper: get terrain at (x, y), return 0 for out-of-bounds
static inline int getTerrain(const uint8_t* data, int w, int h, int x, int y) {
if (x < 0 || x >= w || y < 0 || y >= h) return 0;
return data[y * w + x];
}
// For corner wang sets: each corner is at the junction of 4 cells.
// The corner terrain is the max index among those cells (standard Tiled convention:
// higher-index terrain "wins" at shared corners).
static inline int cornerTerrain(int a, int b, int c, int d) {
int m = a;
if (b > m) m = b;
if (c > m) m = c;
if (d > m) m = d;
return m;
}
std::vector<int> resolveWangTerrain(
const uint8_t* terrain_data, int width, int height,
const WangSet& wang_set)
{
std::vector<int> result(width * height, -1);
if (wang_set.type == WangSetType::Corner) {
// Corner set: wangid layout is [top, TR, right, BR, bottom, BL, left, TL]
// For corner sets, only even indices matter: [_, TR, _, BR, _, BL, _, TL]
// i.e. indices 1, 3, 5, 7
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
// Top-left corner: junction of (x-1,y-1), (x,y-1), (x-1,y), (x,y)
int tl = cornerTerrain(
getTerrain(terrain_data, width, height, x-1, y-1),
getTerrain(terrain_data, width, height, x, y-1),
getTerrain(terrain_data, width, height, x-1, y),
getTerrain(terrain_data, width, height, x, y));
// Top-right corner: junction of (x,y-1), (x+1,y-1), (x,y), (x+1,y)
int tr = cornerTerrain(
getTerrain(terrain_data, width, height, x, y-1),
getTerrain(terrain_data, width, height, x+1, y-1),
getTerrain(terrain_data, width, height, x, y),
getTerrain(terrain_data, width, height, x+1, y));
// Bottom-right corner: junction of (x,y), (x+1,y), (x,y+1), (x+1,y+1)
int br = cornerTerrain(
getTerrain(terrain_data, width, height, x, y),
getTerrain(terrain_data, width, height, x+1, y),
getTerrain(terrain_data, width, height, x, y+1),
getTerrain(terrain_data, width, height, x+1, y+1));
// Bottom-left corner: junction of (x-1,y), (x,y), (x-1,y+1), (x,y+1)
int bl = cornerTerrain(
getTerrain(terrain_data, width, height, x-1, y),
getTerrain(terrain_data, width, height, x, y),
getTerrain(terrain_data, width, height, x-1, y+1),
getTerrain(terrain_data, width, height, x, y+1));
// Pack: [0, TR, 0, BR, 0, BL, 0, TL]
std::array<int, 8> wid = {0, tr, 0, br, 0, bl, 0, tl};
uint64_t key = WangSet::packWangId(wid);
auto it = wang_set.wang_lookup.find(key);
if (it != wang_set.wang_lookup.end()) {
result[y * width + x] = it->second;
}
}
}
}
else if (wang_set.type == WangSetType::Edge) {
// Edge set: wangid layout is [top, TR, right, BR, bottom, BL, left, TL]
// For edge sets, only even indices matter: [top, _, right, _, bottom, _, left, _]
// i.e. indices 0, 2, 4, 6
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int top = getTerrain(terrain_data, width, height, x, y-1);
int right = getTerrain(terrain_data, width, height, x+1, y);
int bottom = getTerrain(terrain_data, width, height, x, y+1);
int left = getTerrain(terrain_data, width, height, x-1, y);
// Pack: [top, 0, right, 0, bottom, 0, left, 0]
std::array<int, 8> wid = {top, 0, right, 0, bottom, 0, left, 0};
uint64_t key = WangSet::packWangId(wid);
auto it = wang_set.wang_lookup.find(key);
if (it != wang_set.wang_lookup.end()) {
result[y * width + x] = it->second;
}
}
}
}
else {
// Mixed: use all 8 values (both edges and corners)
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int top = getTerrain(terrain_data, width, height, x, y-1);
int right = getTerrain(terrain_data, width, height, x+1, y);
int bottom = getTerrain(terrain_data, width, height, x, y+1);
int left = getTerrain(terrain_data, width, height, x-1, y);
int tl = cornerTerrain(
getTerrain(terrain_data, width, height, x-1, y-1),
getTerrain(terrain_data, width, height, x, y-1),
getTerrain(terrain_data, width, height, x-1, y),
getTerrain(terrain_data, width, height, x, y));
int tr = cornerTerrain(
getTerrain(terrain_data, width, height, x, y-1),
getTerrain(terrain_data, width, height, x+1, y-1),
getTerrain(terrain_data, width, height, x, y),
getTerrain(terrain_data, width, height, x+1, y));
int br = cornerTerrain(
getTerrain(terrain_data, width, height, x, y),
getTerrain(terrain_data, width, height, x+1, y),
getTerrain(terrain_data, width, height, x, y+1),
getTerrain(terrain_data, width, height, x+1, y+1));
int bl = cornerTerrain(
getTerrain(terrain_data, width, height, x-1, y),
getTerrain(terrain_data, width, height, x, y),
getTerrain(terrain_data, width, height, x-1, y+1),
getTerrain(terrain_data, width, height, x, y+1));
std::array<int, 8> wid = {top, tr, right, br, bottom, bl, left, tl};
uint64_t key = WangSet::packWangId(wid);
auto it = wang_set.wang_lookup.find(key);
if (it != wang_set.wang_lookup.end()) {
result[y * width + x] = it->second;
}
}
}
}
return result;
}
} // namespace tiled
} // namespace mcrf

17
src/tiled/WangResolve.h Normal file
View file

@ -0,0 +1,17 @@
#pragma once
#include "TiledTypes.h"
#include <vector>
#include <cstdint>
namespace mcrf {
namespace tiled {
// Resolve terrain data (from DiscreteMap) to tile indices using a WangSet.
// Returns a vector of tile IDs (one per cell). -1 means no matching tile found.
// terrain_data: row-major uint8 array, width*height elements
std::vector<int> resolveWangTerrain(
const uint8_t* terrain_data, int width, int height,
const WangSet& wang_set);
} // namespace tiled
} // namespace mcrf