McRogueFace/src/tiled/PyWangSet.cpp

266 lines
9.3 KiB
C++
Raw Normal View History

2026-02-06 21:43:03 -05:00
#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}
};