Tiled XML/JSON import support
This commit is contained in:
parent
71cd2b9b41
commit
b093e087e1
18 changed files with 3040 additions and 0 deletions
332
src/tiled/PyTileMapFile.cpp
Normal file
332
src/tiled/PyTileMapFile.cpp
Normal 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
81
src/tiled/PyTileMapFile.h
Normal 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
234
src/tiled/PyTileSetFile.cpp
Normal 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
79
src/tiled/PyTileSetFile.h
Normal 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
266
src/tiled/PyWangSet.cpp
Normal 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
72
src/tiled/PyWangSet.h
Normal 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
772
src/tiled/TiledParse.cpp
Normal 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
24
src/tiled/TiledParse.h
Normal 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
186
src/tiled/TiledTypes.h
Normal 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
142
src/tiled/WangResolve.cpp
Normal 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
17
src/tiled/WangResolve.h
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue