Tiled XML/JSON import support
This commit is contained in:
parent
71cd2b9b41
commit
b093e087e1
18 changed files with 3040 additions and 0 deletions
6
.gitmodules
vendored
6
.gitmodules
vendored
|
|
@ -14,3 +14,9 @@
|
||||||
path = modules/libtcod-headless
|
path = modules/libtcod-headless
|
||||||
url = git@github.com:jmccardle/libtcod-headless.git
|
url = git@github.com:jmccardle/libtcod-headless.git
|
||||||
branch = 2.2.1-headless
|
branch = 2.2.1-headless
|
||||||
|
[submodule "modules/RapidXML"]
|
||||||
|
path = modules/RapidXML
|
||||||
|
url = https://github.com/Fe-Bell/RapidXML
|
||||||
|
[submodule "modules/json"]
|
||||||
|
path = modules/json
|
||||||
|
url = git@github.com:nlohmann/json.git
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,9 @@ include_directories(SYSTEM ${CMAKE_SOURCE_DIR}/deps/libtcod)
|
||||||
include_directories(${CMAKE_SOURCE_DIR}/src)
|
include_directories(${CMAKE_SOURCE_DIR}/src)
|
||||||
include_directories(${CMAKE_SOURCE_DIR}/src/3d)
|
include_directories(${CMAKE_SOURCE_DIR}/src/3d)
|
||||||
include_directories(${CMAKE_SOURCE_DIR}/src/platform)
|
include_directories(${CMAKE_SOURCE_DIR}/src/platform)
|
||||||
|
include_directories(${CMAKE_SOURCE_DIR}/src/tiled)
|
||||||
|
include_directories(${CMAKE_SOURCE_DIR}/modules/RapidXML)
|
||||||
|
include_directories(${CMAKE_SOURCE_DIR}/modules/json/single_include)
|
||||||
|
|
||||||
# Python includes: use different paths for Windows vs Linux vs Emscripten
|
# Python includes: use different paths for Windows vs Linux vs Emscripten
|
||||||
if(EMSCRIPTEN)
|
if(EMSCRIPTEN)
|
||||||
|
|
|
||||||
1
modules/RapidXML
Submodule
1
modules/RapidXML
Submodule
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 3a42082084509e9efb58dcef17b1ad5860dab6ac
|
||||||
1
modules/json
Submodule
1
modules/json
Submodule
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 21b53746c9d73d314d5de454e2e7cddd20cbbe5d
|
||||||
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
|
||||||
167
tests/demo/screens/tiled_analysis.py
Normal file
167
tests/demo/screens/tiled_analysis.py
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
# tiled_analysis.py - Wang set adjacency analysis utility
|
||||||
|
# Prints adjacency graph, terrain chains, and valid/invalid pair counts
|
||||||
|
# for exploring tileset Wang transition rules.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# cd build && ./mcrogueface --headless --exec ../tests/demo/screens/tiled_analysis.py
|
||||||
|
|
||||||
|
import mcrfpy
|
||||||
|
import sys
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
# -- Configuration --------------------------------------------------------
|
||||||
|
PUNY_BASE = "/home/john/Development/7DRL2026_Liber_Noster_jmccardle/assets_sources/PUNY_WORLD_v1/PUNY_WORLD_v1"
|
||||||
|
TSX_PATH = PUNY_BASE + "/Tiled/punyworld-overworld-tiles.tsx"
|
||||||
|
WANG_SET_NAME = "overworld"
|
||||||
|
|
||||||
|
|
||||||
|
def analyze_wang_set(tileset, wang_set_name):
|
||||||
|
"""Analyze a Wang set and print adjacency information."""
|
||||||
|
ws = tileset.wang_set(wang_set_name)
|
||||||
|
T = ws.terrain_enum()
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print(f"Wang Set Analysis: {ws.name}")
|
||||||
|
print(f" Type: {ws.type}")
|
||||||
|
print(f" Colors: {ws.color_count}")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# List all terrains
|
||||||
|
terrains = [t for t in T if t != T.NONE]
|
||||||
|
print(f"\nTerrains ({len(terrains)}):")
|
||||||
|
for t in terrains:
|
||||||
|
print(f" {t.value:3d}: {t.name}")
|
||||||
|
|
||||||
|
# Test all pairs for valid Wang transitions using 2x2 grids
|
||||||
|
adjacency = defaultdict(set) # terrain -> set of valid neighbors
|
||||||
|
valid_pairs = []
|
||||||
|
invalid_pairs = []
|
||||||
|
|
||||||
|
for a in terrains:
|
||||||
|
for b in terrains:
|
||||||
|
if b.value <= a.value:
|
||||||
|
continue
|
||||||
|
# Create a 2x2 map: columns of A and B
|
||||||
|
dm = mcrfpy.DiscreteMap((2, 2))
|
||||||
|
dm.set(0, 0, a)
|
||||||
|
dm.set(1, 0, b)
|
||||||
|
dm.set(0, 1, a)
|
||||||
|
dm.set(1, 1, b)
|
||||||
|
results = ws.resolve(dm)
|
||||||
|
has_invalid = any(r == -1 for r in results)
|
||||||
|
if not has_invalid:
|
||||||
|
valid_pairs.append((a, b))
|
||||||
|
adjacency[a.name].add(b.name)
|
||||||
|
adjacency[b.name].add(a.name)
|
||||||
|
else:
|
||||||
|
invalid_pairs.append((a, b))
|
||||||
|
|
||||||
|
# Print adjacency graph
|
||||||
|
print(f"\nAdjacency Graph ({len(valid_pairs)} valid pairs):")
|
||||||
|
print("-" * 40)
|
||||||
|
for t in terrains:
|
||||||
|
neighbors = sorted(adjacency.get(t.name, set()))
|
||||||
|
if neighbors:
|
||||||
|
print(f" {t.name}")
|
||||||
|
for n in neighbors:
|
||||||
|
print(f" <-> {n}")
|
||||||
|
else:
|
||||||
|
print(f" {t.name} (ISOLATED - no valid neighbors)")
|
||||||
|
|
||||||
|
# Find terrain chains (connected components)
|
||||||
|
print(f"\nTerrain Chains (connected components):")
|
||||||
|
print("-" * 40)
|
||||||
|
visited = set()
|
||||||
|
chains = []
|
||||||
|
|
||||||
|
def bfs(start):
|
||||||
|
chain = []
|
||||||
|
queue = [start]
|
||||||
|
while queue:
|
||||||
|
node = queue.pop(0)
|
||||||
|
if node in visited:
|
||||||
|
continue
|
||||||
|
visited.add(node)
|
||||||
|
chain.append(node)
|
||||||
|
for neighbor in sorted(adjacency.get(node, set())):
|
||||||
|
if neighbor not in visited:
|
||||||
|
queue.append(neighbor)
|
||||||
|
return chain
|
||||||
|
|
||||||
|
for t in terrains:
|
||||||
|
if t.name not in visited:
|
||||||
|
chain = bfs(t.name)
|
||||||
|
if chain:
|
||||||
|
chains.append(chain)
|
||||||
|
|
||||||
|
for i, chain in enumerate(chains):
|
||||||
|
print(f"\n Chain {i+1}: {len(chain)} terrains")
|
||||||
|
for name in chain:
|
||||||
|
neighbors = sorted(adjacency.get(name, set()))
|
||||||
|
connections = ", ".join(neighbors) if neighbors else "(none)"
|
||||||
|
print(f" {name} -> [{connections}]")
|
||||||
|
|
||||||
|
# Find linear paths within chains
|
||||||
|
print(f"\nLinear Paths (degree-1 endpoints to degree-1 endpoints):")
|
||||||
|
print("-" * 40)
|
||||||
|
for chain in chains:
|
||||||
|
# Find nodes with degree 1 (endpoints) or degree > 2 (hubs)
|
||||||
|
endpoints = [n for n in chain if len(adjacency.get(n, set())) == 1]
|
||||||
|
hubs = [n for n in chain if len(adjacency.get(n, set())) > 2]
|
||||||
|
|
||||||
|
if endpoints:
|
||||||
|
print(f" Endpoints: {', '.join(endpoints)}")
|
||||||
|
if hubs:
|
||||||
|
print(f" Hubs: {', '.join(hubs)} (branch points)")
|
||||||
|
|
||||||
|
# Trace from each endpoint
|
||||||
|
for ep in endpoints:
|
||||||
|
path = [ep]
|
||||||
|
current = ep
|
||||||
|
prev = None
|
||||||
|
while True:
|
||||||
|
neighbors = adjacency.get(current, set()) - {prev} if prev else adjacency.get(current, set())
|
||||||
|
if len(neighbors) == 0:
|
||||||
|
break
|
||||||
|
if len(neighbors) > 1:
|
||||||
|
path.append(f"({current} branches)")
|
||||||
|
break
|
||||||
|
nxt = list(neighbors)[0]
|
||||||
|
path.append(nxt)
|
||||||
|
prev = current
|
||||||
|
current = nxt
|
||||||
|
if len(adjacency.get(current, set())) != 2:
|
||||||
|
break # reached endpoint or hub
|
||||||
|
print(f" Path: {' -> '.join(path)}")
|
||||||
|
|
||||||
|
# Summary statistics
|
||||||
|
total_possible = len(terrains) * (len(terrains) - 1) // 2
|
||||||
|
print(f"\nSummary:")
|
||||||
|
print(f" Total terrain types: {len(terrains)}")
|
||||||
|
print(f" Valid transitions: {len(valid_pairs)} / {total_possible} "
|
||||||
|
f"({100*len(valid_pairs)/total_possible:.1f}%)")
|
||||||
|
print(f" Invalid transitions: {len(invalid_pairs)}")
|
||||||
|
print(f" Connected components: {len(chains)}")
|
||||||
|
|
||||||
|
# Print invalid pairs for reference
|
||||||
|
if invalid_pairs:
|
||||||
|
print(f"\nInvalid Pairs ({len(invalid_pairs)}):")
|
||||||
|
for a, b in invalid_pairs:
|
||||||
|
print(f" {a.name} X {b.name}")
|
||||||
|
|
||||||
|
return valid_pairs, invalid_pairs, chains
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("Loading tileset...")
|
||||||
|
tileset = mcrfpy.TileSetFile(TSX_PATH)
|
||||||
|
print(f" {tileset.name}: {tileset.tile_count} tiles "
|
||||||
|
f"({tileset.columns} cols, {tileset.tile_width}x{tileset.tile_height}px)")
|
||||||
|
|
||||||
|
analyze_wang_set(tileset, WANG_SET_NAME)
|
||||||
|
print("\nDone!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
sys.exit(0)
|
||||||
504
tests/demo/screens/tiled_demo.py
Normal file
504
tests/demo/screens/tiled_demo.py
Normal file
|
|
@ -0,0 +1,504 @@
|
||||||
|
# tiled_demo.py - Visual demo of Tiled integration
|
||||||
|
# Shows premade maps, Wang auto-tiling, and procgen terrain
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# Headless: cd build && ./mcrogueface --headless --exec ../tests/demo/screens/tiled_demo.py
|
||||||
|
# Interactive: cd build && ./mcrogueface --exec ../tests/demo/screens/tiled_demo.py
|
||||||
|
|
||||||
|
import mcrfpy
|
||||||
|
from mcrfpy import automation
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# -- Asset Paths -------------------------------------------------------
|
||||||
|
PUNY_BASE = "/home/john/Development/7DRL2026_Liber_Noster_jmccardle/assets_sources/PUNY_WORLD_v1/PUNY_WORLD_v1"
|
||||||
|
TSX_PATH = PUNY_BASE + "/Tiled/punyworld-overworld-tiles.tsx"
|
||||||
|
|
||||||
|
# -- Load Shared Assets ------------------------------------------------
|
||||||
|
print("Loading Puny World tileset...")
|
||||||
|
tileset = mcrfpy.TileSetFile(TSX_PATH)
|
||||||
|
texture = tileset.to_texture()
|
||||||
|
overworld_ws = tileset.wang_set("overworld")
|
||||||
|
Terrain = overworld_ws.terrain_enum()
|
||||||
|
|
||||||
|
print(f" Tileset: {tileset.name}")
|
||||||
|
print(f" Tiles: {tileset.tile_count} ({tileset.columns} cols, {tileset.tile_width}x{tileset.tile_height}px)")
|
||||||
|
print(f" Wang set: {overworld_ws.name} ({overworld_ws.type}, {overworld_ws.color_count} colors)")
|
||||||
|
print(f" Terrain enum members: {[t.name for t in Terrain]}")
|
||||||
|
|
||||||
|
# -- Helper: Iterative terrain expansion ----------------------------------
|
||||||
|
def iterative_terrain(hm, wang_set, width, height, passes):
|
||||||
|
"""Build a DiscreteMap by iteratively splitting terrains outward from
|
||||||
|
a valid binary map. Each pass splits one terrain on each end of the
|
||||||
|
chain, validates with wang_set.resolve(), and reverts invalid cells
|
||||||
|
to their previous value.
|
||||||
|
|
||||||
|
hm: HeightMap (normalized 0-1)
|
||||||
|
passes: list of (threshold, lo_old, lo_new, hi_old, hi_new) tuples.
|
||||||
|
Each pass says: cells currently == lo_old with height < threshold
|
||||||
|
become lo_new; cells currently == hi_old with height >= threshold
|
||||||
|
become hi_new.
|
||||||
|
|
||||||
|
Returns (DiscreteMap, stats_dict).
|
||||||
|
"""
|
||||||
|
dm = mcrfpy.DiscreteMap((width, height))
|
||||||
|
|
||||||
|
# Pass 0: binary split - everything is one of two terrains
|
||||||
|
p0 = passes[0]
|
||||||
|
thresh, lo_terrain, hi_terrain = p0
|
||||||
|
for y in range(height):
|
||||||
|
for x in range(width):
|
||||||
|
if hm.get(x, y) < thresh:
|
||||||
|
dm.set(x, y, int(lo_terrain))
|
||||||
|
else:
|
||||||
|
dm.set(x, y, int(hi_terrain))
|
||||||
|
|
||||||
|
# Validate pass 0 and fix any invalid cells (rare edge cases like
|
||||||
|
# checkerboard patterns at the binary boundary)
|
||||||
|
results = wang_set.resolve(dm)
|
||||||
|
inv = sum(1 for r in results if r == -1)
|
||||||
|
if inv > 0:
|
||||||
|
# Fix by flipping invalid cells to the other terrain
|
||||||
|
for y in range(height):
|
||||||
|
for x in range(width):
|
||||||
|
if results[y * width + x] == -1:
|
||||||
|
val = dm.get(x, y)
|
||||||
|
if val == int(lo_terrain):
|
||||||
|
dm.set(x, y, int(hi_terrain))
|
||||||
|
else:
|
||||||
|
dm.set(x, y, int(lo_terrain))
|
||||||
|
results2 = wang_set.resolve(dm)
|
||||||
|
inv = sum(1 for r in results2 if r == -1)
|
||||||
|
stats = {"pass0_invalid": inv}
|
||||||
|
|
||||||
|
# Subsequent passes: split outward
|
||||||
|
for pi, (thresh, lo_old, lo_new, hi_old, hi_new) in enumerate(passes[1:], 1):
|
||||||
|
# Save current state so we can revert invalid cells
|
||||||
|
prev = [dm.get(x, y) for y in range(height) for x in range(width)]
|
||||||
|
|
||||||
|
# Track which cells were changed this pass
|
||||||
|
changed = set()
|
||||||
|
for y in range(height):
|
||||||
|
for x in range(width):
|
||||||
|
val = dm.get(x, y)
|
||||||
|
h = hm.get(x, y)
|
||||||
|
if val == int(lo_old) and h < thresh:
|
||||||
|
dm.set(x, y, int(lo_new))
|
||||||
|
changed.add((x, y))
|
||||||
|
elif val == int(hi_old) and h >= thresh:
|
||||||
|
dm.set(x, y, int(hi_new))
|
||||||
|
changed.add((x, y))
|
||||||
|
|
||||||
|
# Iteratively revert changed cells that cause invalid tiles.
|
||||||
|
# A changed cell should be reverted if:
|
||||||
|
# - It is itself invalid, OR
|
||||||
|
# - It is a neighbor of an invalid UN-changed cell (it broke
|
||||||
|
# a pre-existing valid cell by being placed next to it)
|
||||||
|
dirs8 = [(-1,-1),(0,-1),(1,-1),(-1,0),(1,0),(-1,1),(0,1),(1,1)]
|
||||||
|
total_reverted = 0
|
||||||
|
for revert_round in range(30):
|
||||||
|
results = wang_set.resolve(dm)
|
||||||
|
to_revert = set()
|
||||||
|
for y in range(height):
|
||||||
|
for x in range(width):
|
||||||
|
if results[y * width + x] != -1:
|
||||||
|
continue
|
||||||
|
if (x, y) in changed:
|
||||||
|
# This changed cell is invalid - revert it
|
||||||
|
to_revert.add((x, y))
|
||||||
|
else:
|
||||||
|
# Pre-existing cell is now invalid - revert its
|
||||||
|
# changed neighbors to restore it
|
||||||
|
for dx, dy in dirs8:
|
||||||
|
nx, ny = x+dx, y+dy
|
||||||
|
if (nx, ny) in changed:
|
||||||
|
to_revert.add((nx, ny))
|
||||||
|
|
||||||
|
if not to_revert:
|
||||||
|
break
|
||||||
|
|
||||||
|
for (x, y) in to_revert:
|
||||||
|
dm.set(x, y, prev[y * width + x])
|
||||||
|
changed.discard((x, y))
|
||||||
|
total_reverted += 1
|
||||||
|
|
||||||
|
results_final = wang_set.resolve(dm)
|
||||||
|
remaining = sum(1 for r in results_final if r == -1)
|
||||||
|
stats[f"pass{pi}_kept"] = len(changed)
|
||||||
|
stats[f"pass{pi}_reverted"] = total_reverted
|
||||||
|
stats[f"pass{pi}_remaining"] = remaining
|
||||||
|
|
||||||
|
return dm, stats
|
||||||
|
|
||||||
|
|
||||||
|
# -- Helper: Info Panel -------------------------------------------------
|
||||||
|
def make_info_panel(scene, lines, x=560, y=60, w=220, h=None):
|
||||||
|
"""Create a semi-transparent info panel with text lines."""
|
||||||
|
if h is None:
|
||||||
|
h = len(lines) * 22 + 20
|
||||||
|
panel = mcrfpy.Frame(pos=(x, y), size=(w, h),
|
||||||
|
fill_color=mcrfpy.Color(20, 20, 30, 220),
|
||||||
|
outline_color=mcrfpy.Color(80, 80, 120),
|
||||||
|
outline=1.5)
|
||||||
|
scene.children.append(panel)
|
||||||
|
for i, text in enumerate(lines):
|
||||||
|
cap = mcrfpy.Caption(text=text, pos=(10, 10 + i * 22))
|
||||||
|
cap.fill_color = mcrfpy.Color(200, 200, 220)
|
||||||
|
panel.children.append(cap)
|
||||||
|
return panel
|
||||||
|
|
||||||
|
|
||||||
|
# ======================================================================
|
||||||
|
# SCREEN 1: Premade Tiled Map
|
||||||
|
# ======================================================================
|
||||||
|
print("\nSetting up Screen 1: Premade Map...")
|
||||||
|
scene1 = mcrfpy.Scene("tiled_premade")
|
||||||
|
|
||||||
|
bg1 = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(10, 10, 15))
|
||||||
|
scene1.children.append(bg1)
|
||||||
|
|
||||||
|
title1 = mcrfpy.Caption(text="Premade Tiled Map (50x50, 3 layers)", pos=(20, 10))
|
||||||
|
title1.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
scene1.children.append(title1)
|
||||||
|
|
||||||
|
# Load samplemap1
|
||||||
|
tm1 = mcrfpy.TileMapFile(PUNY_BASE + "/Tiled/samplemap1.tmj")
|
||||||
|
print(f" Map: {tm1.width}x{tm1.height}, layers: {tm1.tile_layer_names}")
|
||||||
|
|
||||||
|
grid1 = mcrfpy.Grid(grid_size=(tm1.width, tm1.height),
|
||||||
|
pos=(20, 50), size=(520, 520), layers=[])
|
||||||
|
grid1.fill_color = mcrfpy.Color(30, 30, 50)
|
||||||
|
|
||||||
|
# Add a tile layer for each map layer, bottom-up z ordering
|
||||||
|
layer_names_1 = tm1.tile_layer_names
|
||||||
|
for i, name in enumerate(layer_names_1):
|
||||||
|
z = -(len(layer_names_1) - i)
|
||||||
|
layer = mcrfpy.TileLayer(name=name, z_index=z, texture=texture)
|
||||||
|
grid1.add_layer(layer)
|
||||||
|
tm1.apply_to_tile_layer(layer, name, tileset_index=0)
|
||||||
|
print(f" Applied layer '{name}' (z_index={z})")
|
||||||
|
|
||||||
|
# Center camera on map center (pixels = tiles * tile_size)
|
||||||
|
grid1.center = (tm1.width * tileset.tile_width // 2,
|
||||||
|
tm1.height * tileset.tile_height // 2)
|
||||||
|
scene1.children.append(grid1)
|
||||||
|
|
||||||
|
make_info_panel(scene1, [
|
||||||
|
f"Tileset: {tileset.name}",
|
||||||
|
f"Tile size: {tileset.tile_width}x{tileset.tile_height}",
|
||||||
|
f"Tile count: {tileset.tile_count}",
|
||||||
|
f"Map size: {tm1.width}x{tm1.height}",
|
||||||
|
"",
|
||||||
|
"Layers:",
|
||||||
|
] + [f" {name}" for name in layer_names_1] + [
|
||||||
|
"",
|
||||||
|
"Wang sets:",
|
||||||
|
f" {overworld_ws.name} ({overworld_ws.type})",
|
||||||
|
f" pathways (edge)",
|
||||||
|
])
|
||||||
|
|
||||||
|
nav1 = mcrfpy.Caption(text="[1] Premade [2] Procgen [3] Side-by-Side [ESC] Quit", pos=(20, 740))
|
||||||
|
nav1.fill_color = mcrfpy.Color(120, 120, 150)
|
||||||
|
scene1.children.append(nav1)
|
||||||
|
|
||||||
|
|
||||||
|
# ======================================================================
|
||||||
|
# SCREEN 2: Procedural Wang Auto-Tile (2-layer approach)
|
||||||
|
# ======================================================================
|
||||||
|
print("\nSetting up Screen 2: Procgen Wang Terrain (2-layer)...")
|
||||||
|
scene2 = mcrfpy.Scene("tiled_procgen")
|
||||||
|
|
||||||
|
bg2 = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(10, 10, 15))
|
||||||
|
scene2.children.append(bg2)
|
||||||
|
|
||||||
|
title2 = mcrfpy.Caption(text="Procgen Wang Auto-Tile (60x60, 2 layers)", pos=(20, 10))
|
||||||
|
title2.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
scene2.children.append(title2)
|
||||||
|
|
||||||
|
W, H = 60, 60
|
||||||
|
T = Terrain # shorthand
|
||||||
|
|
||||||
|
# Generate terrain heightmap using NoiseSource
|
||||||
|
noise = mcrfpy.NoiseSource(dimensions=2, seed=42)
|
||||||
|
hm = noise.sample(size=(W, H), mode="fbm", octaves=4, world_size=(4.0, 4.0))
|
||||||
|
hm.normalize(0.0, 1.0)
|
||||||
|
|
||||||
|
# -- Base terrain: iterative expansion from binary map --
|
||||||
|
# Pass 0: binary split at median -> SEAWATER_LIGHT / SAND
|
||||||
|
# Pass 1: split outward -> SEAWATER_MEDIUM from LIGHT, GRASS from SAND
|
||||||
|
# Pass 2: split outward -> SEAWATER_DEEP from MEDIUM, CLIFF from GRASS
|
||||||
|
base_passes = [
|
||||||
|
# Pass 0: (threshold, lo_terrain, hi_terrain)
|
||||||
|
(0.45, T.SEAWATER_LIGHT, T.SAND),
|
||||||
|
# Pass 1+: (threshold, lo_old, lo_new, hi_old, hi_new)
|
||||||
|
(0.30, T.SEAWATER_LIGHT, T.SEAWATER_MEDIUM, T.SAND, T.GRASS),
|
||||||
|
(0.20, T.SEAWATER_MEDIUM, T.SEAWATER_DEEP, T.GRASS, T.CLIFF),
|
||||||
|
]
|
||||||
|
base_dm, base_stats = iterative_terrain(hm, overworld_ws, W, H, base_passes)
|
||||||
|
base_dm.enum_type = T
|
||||||
|
print(f" Base terrain stats: {base_stats}")
|
||||||
|
|
||||||
|
# -- Tree overlay: separate noise, binary TREES/AIR --
|
||||||
|
tree_noise = mcrfpy.NoiseSource(dimensions=2, seed=999)
|
||||||
|
tree_hm = tree_noise.sample(size=(W, H), mode="fbm", octaves=3, world_size=(6.0, 6.0))
|
||||||
|
tree_hm.normalize(0.0, 1.0)
|
||||||
|
|
||||||
|
overlay_dm = mcrfpy.DiscreteMap((W, H))
|
||||||
|
overlay_dm.enum_type = T
|
||||||
|
for y in range(H):
|
||||||
|
for x in range(W):
|
||||||
|
base_val = base_dm.get(x, y)
|
||||||
|
tree_h = tree_hm.get(x, y)
|
||||||
|
# Trees only on GRASS, driven by separate noise
|
||||||
|
if base_val == int(T.GRASS) and tree_h > 0.45:
|
||||||
|
overlay_dm.set(x, y, int(T.TREES))
|
||||||
|
else:
|
||||||
|
overlay_dm.set(x, y, int(T.AIR))
|
||||||
|
|
||||||
|
# Validate overlay and revert invalid to AIR
|
||||||
|
overlay_results = overworld_ws.resolve(overlay_dm)
|
||||||
|
overlay_reverted = 0
|
||||||
|
for y in range(H):
|
||||||
|
for x in range(W):
|
||||||
|
if overlay_results[y * W + x] == -1:
|
||||||
|
overlay_dm.set(x, y, int(T.AIR))
|
||||||
|
overlay_reverted += 1
|
||||||
|
print(f" Overlay: {overlay_reverted} tree cells reverted to AIR")
|
||||||
|
|
||||||
|
# Count terrain distribution
|
||||||
|
terrain_counts = {}
|
||||||
|
for t in T:
|
||||||
|
if t == T.NONE:
|
||||||
|
continue
|
||||||
|
c = base_dm.count(int(t))
|
||||||
|
if c > 0:
|
||||||
|
terrain_counts[t.name] = c
|
||||||
|
tree_count = overlay_dm.count(int(T.TREES))
|
||||||
|
terrain_counts["TREES(overlay)"] = tree_count
|
||||||
|
|
||||||
|
print(f" Terrain distribution: {terrain_counts}")
|
||||||
|
|
||||||
|
# Create grid with 2 layers and apply Wang auto-tiling
|
||||||
|
grid2 = mcrfpy.Grid(grid_size=(W, H), pos=(20, 50), size=(520, 520), layers=[])
|
||||||
|
grid2.fill_color = mcrfpy.Color(30, 30, 50)
|
||||||
|
|
||||||
|
base_layer2 = mcrfpy.TileLayer(name="base", z_index=-2, texture=texture)
|
||||||
|
grid2.add_layer(base_layer2)
|
||||||
|
overworld_ws.apply(base_dm, base_layer2)
|
||||||
|
|
||||||
|
overlay_layer2 = mcrfpy.TileLayer(name="trees", z_index=-1, texture=texture)
|
||||||
|
grid2.add_layer(overlay_layer2)
|
||||||
|
overworld_ws.apply(overlay_dm, overlay_layer2)
|
||||||
|
|
||||||
|
# Post-process overlay: AIR resolves to an opaque tile, set to -1 (transparent)
|
||||||
|
for y in range(H):
|
||||||
|
for x in range(W):
|
||||||
|
if overlay_dm.get(x, y) == int(T.AIR):
|
||||||
|
overlay_layer2.set((x, y), -1)
|
||||||
|
|
||||||
|
grid2.center = (W * tileset.tile_width // 2, H * tileset.tile_height // 2)
|
||||||
|
scene2.children.append(grid2)
|
||||||
|
|
||||||
|
# Info panel
|
||||||
|
info_lines = [
|
||||||
|
"Iterative terrain expansion",
|
||||||
|
f"Seed: 42 (base), 999 (trees)",
|
||||||
|
f"Grid: {W}x{H}, 2 layers",
|
||||||
|
"",
|
||||||
|
"Base (3 passes):",
|
||||||
|
]
|
||||||
|
for name in ["SEAWATER_DEEP", "SEAWATER_MEDIUM", "SEAWATER_LIGHT",
|
||||||
|
"SAND", "GRASS", "CLIFF"]:
|
||||||
|
count = terrain_counts.get(name, 0)
|
||||||
|
info_lines.append(f" {name}: {count}")
|
||||||
|
info_lines.append("")
|
||||||
|
info_lines.append("Tree Overlay:")
|
||||||
|
info_lines.append(f" TREES: {tree_count}")
|
||||||
|
info_lines.append(f" reverted: {overlay_reverted}")
|
||||||
|
|
||||||
|
make_info_panel(scene2, info_lines)
|
||||||
|
|
||||||
|
nav2 = mcrfpy.Caption(text="[1] Premade [2] Procgen [3] Side-by-Side [ESC] Quit", pos=(20, 740))
|
||||||
|
nav2.fill_color = mcrfpy.Color(120, 120, 150)
|
||||||
|
scene2.children.append(nav2)
|
||||||
|
|
||||||
|
|
||||||
|
# ======================================================================
|
||||||
|
# SCREEN 3: Side-by-Side Comparison
|
||||||
|
# ======================================================================
|
||||||
|
print("\nSetting up Screen 3: Side-by-Side...")
|
||||||
|
scene3 = mcrfpy.Scene("tiled_compare")
|
||||||
|
|
||||||
|
bg3 = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(10, 10, 15))
|
||||||
|
scene3.children.append(bg3)
|
||||||
|
|
||||||
|
title3 = mcrfpy.Caption(text="Premade vs Procedural", pos=(20, 10))
|
||||||
|
title3.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
scene3.children.append(title3)
|
||||||
|
|
||||||
|
# Left: Premade map (samplemap2, 30x30)
|
||||||
|
tm2 = mcrfpy.TileMapFile(PUNY_BASE + "/Tiled/samplemap2.tmj")
|
||||||
|
print(f" Map2: {tm2.width}x{tm2.height}, layers: {tm2.tile_layer_names}")
|
||||||
|
|
||||||
|
left_label = mcrfpy.Caption(text="Premade (samplemap2)", pos=(20, 38))
|
||||||
|
left_label.fill_color = mcrfpy.Color(180, 220, 255)
|
||||||
|
scene3.children.append(left_label)
|
||||||
|
|
||||||
|
grid_left = mcrfpy.Grid(grid_size=(tm2.width, tm2.height),
|
||||||
|
pos=(20, 60), size=(380, 380), layers=[])
|
||||||
|
grid_left.fill_color = mcrfpy.Color(30, 30, 50)
|
||||||
|
|
||||||
|
for i, name in enumerate(tm2.tile_layer_names):
|
||||||
|
z = -(len(tm2.tile_layer_names) - i)
|
||||||
|
layer = mcrfpy.TileLayer(name=name, z_index=z, texture=texture)
|
||||||
|
grid_left.add_layer(layer)
|
||||||
|
tm2.apply_to_tile_layer(layer, name, tileset_index=0)
|
||||||
|
|
||||||
|
grid_left.center = (tm2.width * tileset.tile_width // 2,
|
||||||
|
tm2.height * tileset.tile_height // 2)
|
||||||
|
scene3.children.append(grid_left)
|
||||||
|
|
||||||
|
# Right: Procgen island
|
||||||
|
right_label = mcrfpy.Caption(text="Procgen Island (2-layer Wang)", pos=(420, 38))
|
||||||
|
right_label.fill_color = mcrfpy.Color(180, 255, 220)
|
||||||
|
scene3.children.append(right_label)
|
||||||
|
|
||||||
|
IW, IH = 30, 30
|
||||||
|
island_noise = mcrfpy.NoiseSource(dimensions=2, seed=7777)
|
||||||
|
island_hm = island_noise.sample(size=(IW, IH), mode="fbm", octaves=3, world_size=(3.0, 3.0))
|
||||||
|
island_hm.normalize(0.0, 1.0)
|
||||||
|
|
||||||
|
# Create island shape: attenuate edges with radial gradient
|
||||||
|
for y in range(IH):
|
||||||
|
for x in range(IW):
|
||||||
|
dx = (x - IW / 2.0) / (IW / 2.0)
|
||||||
|
dy = (y - IH / 2.0) / (IH / 2.0)
|
||||||
|
dist = (dx * dx + dy * dy) ** 0.5
|
||||||
|
falloff = max(0.0, 1.0 - dist * 1.2)
|
||||||
|
h = island_hm.get(x, y) * falloff
|
||||||
|
island_hm[x, y] = h
|
||||||
|
|
||||||
|
island_hm.normalize(0.0, 1.0)
|
||||||
|
|
||||||
|
# Iterative base terrain expansion (same technique as Screen 2)
|
||||||
|
island_passes_def = [
|
||||||
|
(0.40, T.SEAWATER_LIGHT, T.SAND),
|
||||||
|
(0.25, T.SEAWATER_LIGHT, T.SEAWATER_MEDIUM, T.SAND, T.GRASS),
|
||||||
|
(0.15, T.SEAWATER_MEDIUM, T.SEAWATER_DEEP, T.GRASS, T.CLIFF),
|
||||||
|
]
|
||||||
|
island_base_dm, island_stats = iterative_terrain(
|
||||||
|
island_hm, overworld_ws, IW, IH, island_passes_def)
|
||||||
|
island_base_dm.enum_type = T
|
||||||
|
print(f" Island base stats: {island_stats}")
|
||||||
|
|
||||||
|
# Tree overlay with separate noise
|
||||||
|
island_tree_noise = mcrfpy.NoiseSource(dimensions=2, seed=8888)
|
||||||
|
island_tree_hm = island_tree_noise.sample(
|
||||||
|
size=(IW, IH), mode="fbm", octaves=3, world_size=(4.0, 4.0))
|
||||||
|
island_tree_hm.normalize(0.0, 1.0)
|
||||||
|
|
||||||
|
island_overlay_dm = mcrfpy.DiscreteMap((IW, IH))
|
||||||
|
island_overlay_dm.enum_type = T
|
||||||
|
for y in range(IH):
|
||||||
|
for x in range(IW):
|
||||||
|
base_val = island_base_dm.get(x, y)
|
||||||
|
tree_h = island_tree_hm.get(x, y)
|
||||||
|
if base_val == int(T.GRASS) and tree_h > 0.50:
|
||||||
|
island_overlay_dm.set(x, y, int(T.TREES))
|
||||||
|
else:
|
||||||
|
island_overlay_dm.set(x, y, int(T.AIR))
|
||||||
|
|
||||||
|
# Validate overlay
|
||||||
|
island_ov_results = overworld_ws.resolve(island_overlay_dm)
|
||||||
|
for y in range(IH):
|
||||||
|
for x in range(IW):
|
||||||
|
if island_ov_results[y * IW + x] == -1:
|
||||||
|
island_overlay_dm.set(x, y, int(T.AIR))
|
||||||
|
|
||||||
|
grid_right = mcrfpy.Grid(grid_size=(IW, IH),
|
||||||
|
pos=(420, 60), size=(380, 380), layers=[])
|
||||||
|
grid_right.fill_color = mcrfpy.Color(30, 30, 50)
|
||||||
|
|
||||||
|
island_base_layer = mcrfpy.TileLayer(name="island_base", z_index=-2, texture=texture)
|
||||||
|
grid_right.add_layer(island_base_layer)
|
||||||
|
overworld_ws.apply(island_base_dm, island_base_layer)
|
||||||
|
|
||||||
|
island_overlay_layer = mcrfpy.TileLayer(name="island_trees", z_index=-1, texture=texture)
|
||||||
|
grid_right.add_layer(island_overlay_layer)
|
||||||
|
overworld_ws.apply(island_overlay_dm, island_overlay_layer)
|
||||||
|
|
||||||
|
# Post-process: make AIR cells transparent
|
||||||
|
for y in range(IH):
|
||||||
|
for x in range(IW):
|
||||||
|
if island_overlay_dm.get(x, y) == int(T.AIR):
|
||||||
|
island_overlay_layer.set((x, y), -1)
|
||||||
|
|
||||||
|
grid_right.center = (IW * tileset.tile_width // 2, IH * tileset.tile_height // 2)
|
||||||
|
scene3.children.append(grid_right)
|
||||||
|
|
||||||
|
# Info for both
|
||||||
|
make_info_panel(scene3, [
|
||||||
|
"Left: Premade Map",
|
||||||
|
f" samplemap2.tmj",
|
||||||
|
f" {tm2.width}x{tm2.height}, {len(tm2.tile_layer_names)} layers",
|
||||||
|
"",
|
||||||
|
"Right: Procgen Island",
|
||||||
|
f" {IW}x{IH}, seed=7777",
|
||||||
|
" Iterative terrain expansion",
|
||||||
|
" 2-layer Wang auto-tile",
|
||||||
|
"",
|
||||||
|
"Same tileset, same engine",
|
||||||
|
"Different workflows",
|
||||||
|
], x=200, y=460, w=400, h=None)
|
||||||
|
|
||||||
|
nav3 = mcrfpy.Caption(text="[1] Premade [2] Procgen [3] Side-by-Side [ESC] Quit", pos=(20, 740))
|
||||||
|
nav3.fill_color = mcrfpy.Color(120, 120, 150)
|
||||||
|
scene3.children.append(nav3)
|
||||||
|
|
||||||
|
|
||||||
|
# ======================================================================
|
||||||
|
# Navigation & Screenshots
|
||||||
|
# ======================================================================
|
||||||
|
scenes = [scene1, scene2, scene3]
|
||||||
|
scene_names = ["premade", "procgen", "compare"]
|
||||||
|
|
||||||
|
# Keyboard navigation (all scenes share the same handler)
|
||||||
|
def on_key(key, action):
|
||||||
|
if action != mcrfpy.InputState.PRESSED:
|
||||||
|
return
|
||||||
|
if key == mcrfpy.Key.NUM_1:
|
||||||
|
mcrfpy.current_scene = scene1
|
||||||
|
elif key == mcrfpy.Key.NUM_2:
|
||||||
|
mcrfpy.current_scene = scene2
|
||||||
|
elif key == mcrfpy.Key.NUM_3:
|
||||||
|
mcrfpy.current_scene = scene3
|
||||||
|
elif key == mcrfpy.Key.ESCAPE:
|
||||||
|
mcrfpy.exit()
|
||||||
|
|
||||||
|
for s in scenes:
|
||||||
|
s.on_key = on_key
|
||||||
|
|
||||||
|
# Detect headless mode and take screenshots synchronously
|
||||||
|
is_headless = False
|
||||||
|
try:
|
||||||
|
win = mcrfpy.Window.get()
|
||||||
|
is_headless = "headless" in str(win).lower()
|
||||||
|
except:
|
||||||
|
is_headless = True
|
||||||
|
|
||||||
|
if is_headless:
|
||||||
|
# Headless: use step() to advance simulation and take screenshots directly
|
||||||
|
for i, (sc, name) in enumerate(zip(scenes, scene_names)):
|
||||||
|
mcrfpy.current_scene = sc
|
||||||
|
# Step a few frames to let the scene render
|
||||||
|
for _ in range(3):
|
||||||
|
mcrfpy.step(0.016)
|
||||||
|
fname = f"tiled_demo_{name}.png"
|
||||||
|
automation.screenshot(fname)
|
||||||
|
print(f" Screenshot: {fname}")
|
||||||
|
print("\nAll screenshots captured. Done!")
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
# Interactive: start on screen 1
|
||||||
|
mcrfpy.current_scene = scene1
|
||||||
|
print("\nTiled Demo ready!")
|
||||||
|
print("Press [1] [2] [3] to switch screens, [ESC] to quit")
|
||||||
153
tests/unit/wang_resolve_test.py
Normal file
153
tests/unit/wang_resolve_test.py
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
"""Unit tests for WangSet terrain_enum, resolve, and apply"""
|
||||||
|
import mcrfpy
|
||||||
|
import sys
|
||||||
|
|
||||||
|
PASS_COUNT = 0
|
||||||
|
FAIL_COUNT = 0
|
||||||
|
|
||||||
|
def check(condition, msg):
|
||||||
|
global PASS_COUNT, FAIL_COUNT
|
||||||
|
if condition:
|
||||||
|
PASS_COUNT += 1
|
||||||
|
print(f" PASS: {msg}")
|
||||||
|
else:
|
||||||
|
FAIL_COUNT += 1
|
||||||
|
print(f" FAIL: {msg}")
|
||||||
|
|
||||||
|
def test_terrain_enum():
|
||||||
|
"""Test IntEnum generation from WangSet colors"""
|
||||||
|
print("=== Terrain Enum ===")
|
||||||
|
ts = mcrfpy.TileSetFile("../tests/assets/tiled/test_tileset.tsx")
|
||||||
|
ws = ts.wang_set("terrain")
|
||||||
|
Terrain = ws.terrain_enum()
|
||||||
|
|
||||||
|
check(Terrain is not None, "terrain_enum() returns something")
|
||||||
|
check(hasattr(Terrain, "NONE"), "has NONE member")
|
||||||
|
check(hasattr(Terrain, "GRASS"), "has GRASS member")
|
||||||
|
check(hasattr(Terrain, "DIRT"), "has DIRT member")
|
||||||
|
check(int(Terrain.NONE) == 0, f"NONE = {int(Terrain.NONE)}")
|
||||||
|
check(int(Terrain.GRASS) == 1, f"GRASS = {int(Terrain.GRASS)}")
|
||||||
|
check(int(Terrain.DIRT) == 2, f"DIRT = {int(Terrain.DIRT)}")
|
||||||
|
|
||||||
|
# Check it's an IntEnum
|
||||||
|
import enum
|
||||||
|
check(issubclass(Terrain, enum.IntEnum), "is IntEnum subclass")
|
||||||
|
return Terrain
|
||||||
|
|
||||||
|
def test_enum_with_discrete_map(Terrain):
|
||||||
|
"""Test that terrain enum is compatible with DiscreteMap"""
|
||||||
|
print("\n=== Enum + DiscreteMap ===")
|
||||||
|
dm = mcrfpy.DiscreteMap((4, 4))
|
||||||
|
dm.enum_type = Terrain
|
||||||
|
check(dm.enum_type == Terrain, "DiscreteMap accepts terrain enum")
|
||||||
|
|
||||||
|
# Set values using enum
|
||||||
|
dm.set(0, 0, Terrain.GRASS)
|
||||||
|
dm.set(1, 0, Terrain.DIRT)
|
||||||
|
val = dm.get(0, 0)
|
||||||
|
check(int(val) == int(Terrain.GRASS), f"get(0,0) = {val}")
|
||||||
|
val = dm.get(1, 0)
|
||||||
|
check(int(val) == int(Terrain.DIRT), f"get(1,0) = {val}")
|
||||||
|
|
||||||
|
def test_resolve_uniform():
|
||||||
|
"""Test resolve with uniform terrain"""
|
||||||
|
print("\n=== Resolve Uniform ===")
|
||||||
|
ts = mcrfpy.TileSetFile("../tests/assets/tiled/test_tileset.tsx")
|
||||||
|
ws = ts.wang_set("terrain")
|
||||||
|
|
||||||
|
# All grass (terrain ID 1)
|
||||||
|
dm = mcrfpy.DiscreteMap((3, 3))
|
||||||
|
dm.fill(1) # All grass
|
||||||
|
|
||||||
|
tiles = ws.resolve(dm)
|
||||||
|
check(isinstance(tiles, list), f"resolve returns list: {type(tiles)}")
|
||||||
|
check(len(tiles) == 9, f"resolve length = {len(tiles)}")
|
||||||
|
|
||||||
|
# All cells should map to the "all grass corners" tile (id=0)
|
||||||
|
# wangid [0,1,0,1,0,1,0,1] = tile 0
|
||||||
|
# Note: border cells will see 0 (NONE) on their outer edges, so may not match
|
||||||
|
# Center cell (1,1) sees all grass neighbors -> should be tile 0
|
||||||
|
center = tiles[4] # (1,1) in 3x3
|
||||||
|
check(center == 0, f"center tile (uniform grass) = {center}")
|
||||||
|
|
||||||
|
def test_resolve_mixed():
|
||||||
|
"""Test resolve with mixed terrain"""
|
||||||
|
print("\n=== Resolve Mixed ===")
|
||||||
|
ts = mcrfpy.TileSetFile("../tests/assets/tiled/test_tileset.tsx")
|
||||||
|
ws = ts.wang_set("terrain")
|
||||||
|
|
||||||
|
# Create a 3x3 grid: grass everywhere except center = dirt
|
||||||
|
dm = mcrfpy.DiscreteMap((3, 3))
|
||||||
|
dm.fill(1) # All grass
|
||||||
|
dm.set(1, 1, 2) # Center = dirt
|
||||||
|
|
||||||
|
tiles = ws.resolve(dm)
|
||||||
|
check(len(tiles) == 9, f"resolve length = {len(tiles)}")
|
||||||
|
|
||||||
|
# The center cell has grass neighbors and is dirt itself
|
||||||
|
# Corners depend on the max of surrounding cells
|
||||||
|
center = tiles[4]
|
||||||
|
# Center: all 4 corners should be max(dirt, grass neighbors) = 2 (dirt)
|
||||||
|
# wangid [0,2,0,2,0,2,0,2] = tile 1 (all-dirt)
|
||||||
|
check(center == 1, f"center (dirt surrounded by grass) = {center}")
|
||||||
|
|
||||||
|
def test_resolve_returns_negative_for_unknown():
|
||||||
|
"""Test that unknown wangid combinations return -1"""
|
||||||
|
print("\n=== Unknown WangID ===")
|
||||||
|
ts = mcrfpy.TileSetFile("../tests/assets/tiled/test_tileset.tsx")
|
||||||
|
ws = ts.wang_set("terrain")
|
||||||
|
|
||||||
|
# Use terrain ID 3 which doesn't exist in the wang set
|
||||||
|
dm = mcrfpy.DiscreteMap((2, 2))
|
||||||
|
dm.fill(3) # Terrain 3 not in wang set
|
||||||
|
|
||||||
|
tiles = ws.resolve(dm)
|
||||||
|
# All should be -1 since terrain 3 has no matching wangid
|
||||||
|
all_neg = all(t == -1 for t in tiles)
|
||||||
|
check(all_neg, f"all tiles = -1 for unknown terrain: {tiles}")
|
||||||
|
|
||||||
|
def test_resolve_border_handling():
|
||||||
|
"""Test that border cells handle out-of-bounds correctly"""
|
||||||
|
print("\n=== Border Handling ===")
|
||||||
|
ts = mcrfpy.TileSetFile("../tests/assets/tiled/test_tileset.tsx")
|
||||||
|
ws = ts.wang_set("terrain")
|
||||||
|
|
||||||
|
# 1x1 grid - all neighbors are out-of-bounds (0)
|
||||||
|
dm = mcrfpy.DiscreteMap((1, 1))
|
||||||
|
dm.set(0, 0, 1) # Single grass cell
|
||||||
|
|
||||||
|
tiles = ws.resolve(dm)
|
||||||
|
check(len(tiles) == 1, f"1x1 resolve length = {len(tiles)}")
|
||||||
|
# Corner terrain: max(0, 0, 0, grass) = 1 for each corner -> all grass
|
||||||
|
# wangid [0,1,0,1,0,1,0,1] = tile 0
|
||||||
|
check(tiles[0] == 0, f"1x1 grass tile = {tiles[0]}")
|
||||||
|
|
||||||
|
def test_wang_set_repr():
|
||||||
|
"""Test WangSet repr"""
|
||||||
|
print("\n=== WangSet Repr ===")
|
||||||
|
ts = mcrfpy.TileSetFile("../tests/assets/tiled/test_tileset.tsx")
|
||||||
|
ws = ts.wang_set("terrain")
|
||||||
|
r = repr(ws)
|
||||||
|
check("WangSet" in r, f"repr contains 'WangSet': {r}")
|
||||||
|
check("terrain" in r, f"repr contains name: {r}")
|
||||||
|
check("corner" in r, f"repr contains type: {r}")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
Terrain = test_terrain_enum()
|
||||||
|
test_enum_with_discrete_map(Terrain)
|
||||||
|
test_resolve_uniform()
|
||||||
|
test_resolve_mixed()
|
||||||
|
test_resolve_returns_negative_for_unknown()
|
||||||
|
test_resolve_border_handling()
|
||||||
|
test_wang_set_repr()
|
||||||
|
|
||||||
|
print(f"\n{'='*40}")
|
||||||
|
print(f"Results: {PASS_COUNT} passed, {FAIL_COUNT} failed")
|
||||||
|
if FAIL_COUNT > 0:
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
print("ALL TESTS PASSED")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue