1047 lines
32 KiB
C++
1047 lines
32 KiB
C++
|
|
#include "PyBSP.h"
|
||
|
|
#include "McRFPy_API.h"
|
||
|
|
#include "McRFPy_Doc.h"
|
||
|
|
#include "PyPositionHelper.h"
|
||
|
|
#include "PyHeightMap.h"
|
||
|
|
#include <sstream>
|
||
|
|
#include <cstdlib>
|
||
|
|
#include <ctime>
|
||
|
|
|
||
|
|
// Static storage for Traversal enum
|
||
|
|
PyObject* PyTraversal::traversal_enum_class = nullptr;
|
||
|
|
|
||
|
|
// Traversal order constants
|
||
|
|
enum TraversalOrder {
|
||
|
|
TRAVERSAL_PRE_ORDER = 0,
|
||
|
|
TRAVERSAL_IN_ORDER = 1,
|
||
|
|
TRAVERSAL_POST_ORDER = 2,
|
||
|
|
TRAVERSAL_LEVEL_ORDER = 3,
|
||
|
|
TRAVERSAL_INVERTED_LEVEL_ORDER = 4,
|
||
|
|
};
|
||
|
|
|
||
|
|
// ==================== Traversal Enum ====================
|
||
|
|
|
||
|
|
PyObject* PyTraversal::create_enum_class(PyObject* module) {
|
||
|
|
// 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;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create dict of enum members
|
||
|
|
PyObject* members = PyDict_New();
|
||
|
|
if (!members) {
|
||
|
|
Py_DECREF(int_enum);
|
||
|
|
return NULL;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add traversal order members
|
||
|
|
struct { const char* name; int value; } entries[] = {
|
||
|
|
{"PRE_ORDER", TRAVERSAL_PRE_ORDER},
|
||
|
|
{"IN_ORDER", TRAVERSAL_IN_ORDER},
|
||
|
|
{"POST_ORDER", TRAVERSAL_POST_ORDER},
|
||
|
|
{"LEVEL_ORDER", TRAVERSAL_LEVEL_ORDER},
|
||
|
|
{"INVERTED_LEVEL_ORDER", TRAVERSAL_INVERTED_LEVEL_ORDER},
|
||
|
|
};
|
||
|
|
|
||
|
|
for (const auto& entry : entries) {
|
||
|
|
PyObject* value = PyLong_FromLong(entry.value);
|
||
|
|
if (!value || PyDict_SetItemString(members, entry.name, value) < 0) {
|
||
|
|
Py_XDECREF(value);
|
||
|
|
Py_DECREF(members);
|
||
|
|
Py_DECREF(int_enum);
|
||
|
|
return NULL;
|
||
|
|
}
|
||
|
|
Py_DECREF(value);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Call IntEnum("Traversal", members)
|
||
|
|
PyObject* name = PyUnicode_FromString("Traversal");
|
||
|
|
if (!name) {
|
||
|
|
Py_DECREF(members);
|
||
|
|
Py_DECREF(int_enum);
|
||
|
|
return NULL;
|
||
|
|
}
|
||
|
|
|
||
|
|
PyObject* args = PyTuple_Pack(2, name, members);
|
||
|
|
Py_DECREF(name);
|
||
|
|
Py_DECREF(members);
|
||
|
|
if (!args) {
|
||
|
|
Py_DECREF(int_enum);
|
||
|
|
return NULL;
|
||
|
|
}
|
||
|
|
|
||
|
|
PyObject* traversal_class = PyObject_Call(int_enum, args, NULL);
|
||
|
|
Py_DECREF(args);
|
||
|
|
Py_DECREF(int_enum);
|
||
|
|
|
||
|
|
if (!traversal_class) {
|
||
|
|
return NULL;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Cache reference
|
||
|
|
traversal_enum_class = traversal_class;
|
||
|
|
Py_INCREF(traversal_enum_class);
|
||
|
|
|
||
|
|
// Add to module
|
||
|
|
if (PyModule_AddObject(module, "Traversal", traversal_class) < 0) {
|
||
|
|
Py_DECREF(traversal_class);
|
||
|
|
traversal_enum_class = nullptr;
|
||
|
|
return NULL;
|
||
|
|
}
|
||
|
|
|
||
|
|
return traversal_class;
|
||
|
|
}
|
||
|
|
|
||
|
|
int PyTraversal::from_arg(PyObject* arg, int* out_order) {
|
||
|
|
// Accept None -> default to LEVEL_ORDER
|
||
|
|
if (arg == NULL || arg == Py_None) {
|
||
|
|
*out_order = TRAVERSAL_LEVEL_ORDER;
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Accept Traversal enum member
|
||
|
|
if (traversal_enum_class && PyObject_IsInstance(arg, traversal_enum_class)) {
|
||
|
|
PyObject* value = PyObject_GetAttrString(arg, "value");
|
||
|
|
if (!value) {
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
long val = PyLong_AsLong(value);
|
||
|
|
Py_DECREF(value);
|
||
|
|
if (val == -1 && PyErr_Occurred()) {
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
if (val >= 0 && val <= TRAVERSAL_INVERTED_LEVEL_ORDER) {
|
||
|
|
*out_order = (int)val;
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
PyErr_Format(PyExc_ValueError, "Invalid Traversal value: %ld", val);
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Accept int
|
||
|
|
if (PyLong_Check(arg)) {
|
||
|
|
long val = PyLong_AsLong(arg);
|
||
|
|
if (val == -1 && PyErr_Occurred()) {
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
if (val >= 0 && val <= TRAVERSAL_INVERTED_LEVEL_ORDER) {
|
||
|
|
*out_order = (int)val;
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
PyErr_Format(PyExc_ValueError,
|
||
|
|
"Invalid traversal value: %ld. Must be 0-4 or use mcrfpy.Traversal enum.", val);
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Accept string
|
||
|
|
if (PyUnicode_Check(arg)) {
|
||
|
|
const char* name = PyUnicode_AsUTF8(arg);
|
||
|
|
if (!name) {
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
if (strcmp(name, "pre") == 0 || strcmp(name, "PRE_ORDER") == 0) {
|
||
|
|
*out_order = TRAVERSAL_PRE_ORDER;
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
if (strcmp(name, "in") == 0 || strcmp(name, "IN_ORDER") == 0) {
|
||
|
|
*out_order = TRAVERSAL_IN_ORDER;
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
if (strcmp(name, "post") == 0 || strcmp(name, "POST_ORDER") == 0) {
|
||
|
|
*out_order = TRAVERSAL_POST_ORDER;
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
if (strcmp(name, "level") == 0 || strcmp(name, "LEVEL_ORDER") == 0) {
|
||
|
|
*out_order = TRAVERSAL_LEVEL_ORDER;
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
if (strcmp(name, "level_inverted") == 0 || strcmp(name, "INVERTED_LEVEL_ORDER") == 0) {
|
||
|
|
*out_order = TRAVERSAL_INVERTED_LEVEL_ORDER;
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
PyErr_Format(PyExc_ValueError,
|
||
|
|
"Unknown traversal order: '%s'. Use mcrfpy.Traversal enum or: "
|
||
|
|
"'pre', 'in', 'post', 'level', 'level_inverted'", name);
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
PyErr_SetString(PyExc_TypeError,
|
||
|
|
"Traversal order must be mcrfpy.Traversal enum, string, int, or None");
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ==================== BSP Property Definitions ====================
|
||
|
|
|
||
|
|
PyGetSetDef PyBSP::getsetters[] = {
|
||
|
|
{"bounds", (getter)PyBSP::get_bounds, NULL,
|
||
|
|
MCRF_PROPERTY(bounds, "Root node bounds as ((x, y), (w, h)). Read-only."), NULL},
|
||
|
|
{"root", (getter)PyBSP::get_root, NULL,
|
||
|
|
MCRF_PROPERTY(root, "Reference to the root BSPNode. Read-only."), NULL},
|
||
|
|
{NULL}
|
||
|
|
};
|
||
|
|
|
||
|
|
// ==================== BSP Method Definitions ====================
|
||
|
|
|
||
|
|
PyMethodDef PyBSP::methods[] = {
|
||
|
|
{"split_once", (PyCFunction)PyBSP::split_once, METH_VARARGS | METH_KEYWORDS,
|
||
|
|
MCRF_METHOD(BSP, split_once,
|
||
|
|
MCRF_SIG("(horizontal: bool, position: int)", "BSP"),
|
||
|
|
MCRF_DESC("Split the root node once at the specified position."),
|
||
|
|
MCRF_ARGS_START
|
||
|
|
MCRF_ARG("horizontal", "True for horizontal split, False for vertical")
|
||
|
|
MCRF_ARG("position", "Split coordinate (y for horizontal, x for vertical)")
|
||
|
|
MCRF_RETURNS("BSP: self, for method chaining")
|
||
|
|
)},
|
||
|
|
{"split_recursive", (PyCFunction)PyBSP::split_recursive, METH_VARARGS | METH_KEYWORDS,
|
||
|
|
MCRF_METHOD(BSP, split_recursive,
|
||
|
|
MCRF_SIG("(depth: int, min_size: tuple[int, int], max_ratio: float = 1.5, seed: int = None)", "BSP"),
|
||
|
|
MCRF_DESC("Recursively split to the specified depth."),
|
||
|
|
MCRF_ARGS_START
|
||
|
|
MCRF_ARG("depth", "Maximum recursion depth. Creates up to 2^depth leaves.")
|
||
|
|
MCRF_ARG("min_size", "Minimum (width, height) for a node to be split.")
|
||
|
|
MCRF_ARG("max_ratio", "Maximum aspect ratio before forcing split direction. Default: 1.5.")
|
||
|
|
MCRF_ARG("seed", "Random seed. None for random.")
|
||
|
|
MCRF_RETURNS("BSP: self, for method chaining")
|
||
|
|
)},
|
||
|
|
{"clear", (PyCFunction)PyBSP::clear, METH_NOARGS,
|
||
|
|
MCRF_METHOD(BSP, clear,
|
||
|
|
MCRF_SIG("()", "BSP"),
|
||
|
|
MCRF_DESC("Remove all children, keeping only the root node with original bounds."),
|
||
|
|
MCRF_RETURNS("BSP: self, for method chaining")
|
||
|
|
)},
|
||
|
|
{"leaves", (PyCFunction)PyBSP::leaves, METH_NOARGS,
|
||
|
|
MCRF_METHOD(BSP, leaves,
|
||
|
|
MCRF_SIG("()", "Iterator[BSPNode]"),
|
||
|
|
MCRF_DESC("Iterate all leaf nodes (the actual rooms)."),
|
||
|
|
MCRF_RETURNS("Iterator yielding BSPNode objects")
|
||
|
|
)},
|
||
|
|
{"traverse", (PyCFunction)PyBSP::traverse, METH_VARARGS | METH_KEYWORDS,
|
||
|
|
MCRF_METHOD(BSP, traverse,
|
||
|
|
MCRF_SIG("(order: Traversal = Traversal.LEVEL_ORDER)", "Iterator[BSPNode]"),
|
||
|
|
MCRF_DESC("Iterate all nodes in the specified order."),
|
||
|
|
MCRF_ARGS_START
|
||
|
|
MCRF_ARG("order", "Traversal order from Traversal enum. Default: LEVEL_ORDER.")
|
||
|
|
MCRF_RETURNS("Iterator yielding BSPNode objects")
|
||
|
|
MCRF_NOTE("Orders: PRE_ORDER, IN_ORDER, POST_ORDER, LEVEL_ORDER, INVERTED_LEVEL_ORDER")
|
||
|
|
)},
|
||
|
|
{"find", (PyCFunction)PyBSP::find, METH_VARARGS | METH_KEYWORDS,
|
||
|
|
MCRF_METHOD(BSP, find,
|
||
|
|
MCRF_SIG("(pos: tuple[int, int])", "BSPNode | None"),
|
||
|
|
MCRF_DESC("Find the smallest (deepest) node containing the position."),
|
||
|
|
MCRF_ARGS_START
|
||
|
|
MCRF_ARG("pos", "Position as (x, y) tuple, list, or Vector")
|
||
|
|
MCRF_RETURNS("BSPNode if found, None if position is outside bounds")
|
||
|
|
)},
|
||
|
|
{"to_heightmap", (PyCFunction)PyBSP::to_heightmap, METH_VARARGS | METH_KEYWORDS,
|
||
|
|
MCRF_METHOD(BSP, to_heightmap,
|
||
|
|
MCRF_SIG("(size: tuple[int, int] = None, select: str = 'leaves', shrink: int = 0, value: float = 1.0)", "HeightMap"),
|
||
|
|
MCRF_DESC("Convert BSP node selection to a HeightMap."),
|
||
|
|
MCRF_ARGS_START
|
||
|
|
MCRF_ARG("size", "Output size (width, height). Default: bounds size.")
|
||
|
|
MCRF_ARG("select", "'leaves', 'all', or 'internal'. Default: 'leaves'.")
|
||
|
|
MCRF_ARG("shrink", "Pixels to shrink from each node's bounds. Default: 0.")
|
||
|
|
MCRF_ARG("value", "Value inside selected regions. Default: 1.0.")
|
||
|
|
MCRF_RETURNS("HeightMap with selected regions filled")
|
||
|
|
)},
|
||
|
|
{NULL}
|
||
|
|
};
|
||
|
|
|
||
|
|
// ==================== BSP Implementation ====================
|
||
|
|
|
||
|
|
PyObject* PyBSP::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds)
|
||
|
|
{
|
||
|
|
PyBSPObject* self = (PyBSPObject*)type->tp_alloc(type, 0);
|
||
|
|
if (self) {
|
||
|
|
self->root = nullptr;
|
||
|
|
self->orig_x = 0;
|
||
|
|
self->orig_y = 0;
|
||
|
|
self->orig_w = 0;
|
||
|
|
self->orig_h = 0;
|
||
|
|
}
|
||
|
|
return (PyObject*)self;
|
||
|
|
}
|
||
|
|
|
||
|
|
int PyBSP::init(PyBSPObject* self, PyObject* args, PyObject* kwds)
|
||
|
|
{
|
||
|
|
static const char* keywords[] = {"bounds", nullptr};
|
||
|
|
PyObject* bounds_obj = nullptr;
|
||
|
|
|
||
|
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", const_cast<char**>(keywords),
|
||
|
|
&bounds_obj)) {
|
||
|
|
return -1;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Parse bounds: ((x, y), (w, h))
|
||
|
|
if (!PyTuple_Check(bounds_obj) || PyTuple_Size(bounds_obj) != 2) {
|
||
|
|
PyErr_SetString(PyExc_TypeError, "bounds must be ((x, y), (w, h)) tuple");
|
||
|
|
return -1;
|
||
|
|
}
|
||
|
|
|
||
|
|
PyObject* pos_obj = PyTuple_GetItem(bounds_obj, 0);
|
||
|
|
PyObject* size_obj = PyTuple_GetItem(bounds_obj, 1);
|
||
|
|
|
||
|
|
if (!PyTuple_Check(pos_obj) || PyTuple_Size(pos_obj) != 2 ||
|
||
|
|
!PyTuple_Check(size_obj) || PyTuple_Size(size_obj) != 2) {
|
||
|
|
PyErr_SetString(PyExc_TypeError, "bounds must be ((x, y), (w, h)) tuple");
|
||
|
|
return -1;
|
||
|
|
}
|
||
|
|
|
||
|
|
int x = (int)PyLong_AsLong(PyTuple_GetItem(pos_obj, 0));
|
||
|
|
int y = (int)PyLong_AsLong(PyTuple_GetItem(pos_obj, 1));
|
||
|
|
int w = (int)PyLong_AsLong(PyTuple_GetItem(size_obj, 0));
|
||
|
|
int h = (int)PyLong_AsLong(PyTuple_GetItem(size_obj, 1));
|
||
|
|
|
||
|
|
if (PyErr_Occurred()) {
|
||
|
|
return -1;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (w <= 0 || h <= 0) {
|
||
|
|
PyErr_SetString(PyExc_ValueError, "width and height must be positive");
|
||
|
|
return -1;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Clean up any existing BSP
|
||
|
|
if (self->root) {
|
||
|
|
TCOD_bsp_delete(self->root);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create new BSP with size
|
||
|
|
self->root = TCOD_bsp_new_with_size(x, y, w, h);
|
||
|
|
if (!self->root) {
|
||
|
|
PyErr_SetString(PyExc_MemoryError, "Failed to allocate BSP");
|
||
|
|
return -1;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Store original bounds for clear()
|
||
|
|
self->orig_x = x;
|
||
|
|
self->orig_y = y;
|
||
|
|
self->orig_w = w;
|
||
|
|
self->orig_h = h;
|
||
|
|
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
void PyBSP::dealloc(PyBSPObject* self)
|
||
|
|
{
|
||
|
|
if (self->root) {
|
||
|
|
TCOD_bsp_delete(self->root);
|
||
|
|
self->root = nullptr;
|
||
|
|
}
|
||
|
|
Py_TYPE(self)->tp_free((PyObject*)self);
|
||
|
|
}
|
||
|
|
|
||
|
|
PyObject* PyBSP::repr(PyObject* obj)
|
||
|
|
{
|
||
|
|
PyBSPObject* self = (PyBSPObject*)obj;
|
||
|
|
std::ostringstream ss;
|
||
|
|
|
||
|
|
if (self->root) {
|
||
|
|
// Count leaves
|
||
|
|
int leaf_count = 0;
|
||
|
|
TCOD_bsp_traverse_pre_order(self->root, [](TCOD_bsp_t* node, void* data) -> bool {
|
||
|
|
if (TCOD_bsp_is_leaf(node)) {
|
||
|
|
(*(int*)data)++;
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}, &leaf_count);
|
||
|
|
|
||
|
|
ss << "<BSP (" << self->root->w << " x " << self->root->h << "), "
|
||
|
|
<< leaf_count << " leaves>";
|
||
|
|
} else {
|
||
|
|
ss << "<BSP (uninitialized)>";
|
||
|
|
}
|
||
|
|
|
||
|
|
return PyUnicode_FromString(ss.str().c_str());
|
||
|
|
}
|
||
|
|
|
||
|
|
// Property: bounds
|
||
|
|
PyObject* PyBSP::get_bounds(PyBSPObject* self, void* closure)
|
||
|
|
{
|
||
|
|
if (!self->root) {
|
||
|
|
PyErr_SetString(PyExc_RuntimeError, "BSP not initialized");
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
return Py_BuildValue("((ii)(ii))",
|
||
|
|
self->root->x, self->root->y,
|
||
|
|
self->root->w, self->root->h);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Property: root
|
||
|
|
PyObject* PyBSP::get_root(PyBSPObject* self, void* closure)
|
||
|
|
{
|
||
|
|
if (!self->root) {
|
||
|
|
PyErr_SetString(PyExc_RuntimeError, "BSP not initialized");
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
return PyBSPNode::create(self->root, (PyObject*)self);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Method: split_once(horizontal, position) -> BSP
|
||
|
|
PyObject* PyBSP::split_once(PyBSPObject* self, PyObject* args, PyObject* kwds)
|
||
|
|
{
|
||
|
|
static const char* keywords[] = {"horizontal", "position", nullptr};
|
||
|
|
int horizontal;
|
||
|
|
int position;
|
||
|
|
|
||
|
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "pi", const_cast<char**>(keywords),
|
||
|
|
&horizontal, &position)) {
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!self->root) {
|
||
|
|
PyErr_SetString(PyExc_RuntimeError, "BSP not initialized");
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
TCOD_bsp_split_once(self->root, horizontal ? true : false, position);
|
||
|
|
|
||
|
|
Py_INCREF(self);
|
||
|
|
return (PyObject*)self;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Method: split_recursive(depth, min_size, max_ratio=1.5, seed=None) -> BSP
|
||
|
|
PyObject* PyBSP::split_recursive(PyBSPObject* self, PyObject* args, PyObject* kwds)
|
||
|
|
{
|
||
|
|
static const char* keywords[] = {"depth", "min_size", "max_ratio", "seed", nullptr};
|
||
|
|
int depth;
|
||
|
|
PyObject* min_size_obj = nullptr;
|
||
|
|
float max_ratio = 1.5f;
|
||
|
|
PyObject* seed_obj = nullptr;
|
||
|
|
|
||
|
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "iO|fO", const_cast<char**>(keywords),
|
||
|
|
&depth, &min_size_obj, &max_ratio, &seed_obj)) {
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!self->root) {
|
||
|
|
PyErr_SetString(PyExc_RuntimeError, "BSP not initialized");
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Parse min_size tuple
|
||
|
|
if (!PyTuple_Check(min_size_obj) || PyTuple_Size(min_size_obj) != 2) {
|
||
|
|
PyErr_SetString(PyExc_TypeError, "min_size must be (width, height) tuple");
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
int min_w = (int)PyLong_AsLong(PyTuple_GetItem(min_size_obj, 0));
|
||
|
|
int min_h = (int)PyLong_AsLong(PyTuple_GetItem(min_size_obj, 1));
|
||
|
|
|
||
|
|
if (PyErr_Occurred()) {
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (depth < 0) {
|
||
|
|
PyErr_SetString(PyExc_ValueError, "depth must be non-negative");
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (min_w <= 0 || min_h <= 0) {
|
||
|
|
PyErr_SetString(PyExc_ValueError, "min_size values must be positive");
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create random generator if seed provided
|
||
|
|
TCOD_Random* rnd = nullptr;
|
||
|
|
if (seed_obj != nullptr && seed_obj != Py_None) {
|
||
|
|
if (!PyLong_Check(seed_obj)) {
|
||
|
|
PyErr_SetString(PyExc_TypeError, "seed must be an integer or None");
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
uint32_t seed = (uint32_t)PyLong_AsUnsignedLong(seed_obj);
|
||
|
|
if (PyErr_Occurred()) {
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
rnd = TCOD_random_new_from_seed(TCOD_RNG_MT, seed);
|
||
|
|
}
|
||
|
|
|
||
|
|
TCOD_bsp_split_recursive(self->root, rnd, depth, min_w, min_h, max_ratio, max_ratio);
|
||
|
|
|
||
|
|
if (rnd) {
|
||
|
|
TCOD_random_delete(rnd);
|
||
|
|
}
|
||
|
|
|
||
|
|
Py_INCREF(self);
|
||
|
|
return (PyObject*)self;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Method: clear() -> BSP
|
||
|
|
PyObject* PyBSP::clear(PyBSPObject* self, PyObject* Py_UNUSED(args))
|
||
|
|
{
|
||
|
|
if (!self->root) {
|
||
|
|
PyErr_SetString(PyExc_RuntimeError, "BSP not initialized");
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
TCOD_bsp_remove_sons(self->root);
|
||
|
|
|
||
|
|
// Restore original bounds
|
||
|
|
TCOD_bsp_resize(self->root, self->orig_x, self->orig_y, self->orig_w, self->orig_h);
|
||
|
|
|
||
|
|
Py_INCREF(self);
|
||
|
|
return (PyObject*)self;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ==================== BSP Iteration ====================
|
||
|
|
|
||
|
|
// Traversal callback to collect nodes
|
||
|
|
struct TraversalData {
|
||
|
|
std::vector<TCOD_bsp_t*>* nodes;
|
||
|
|
bool leaves_only;
|
||
|
|
};
|
||
|
|
|
||
|
|
static bool collect_callback(TCOD_bsp_t* node, void* userData) {
|
||
|
|
TraversalData* data = (TraversalData*)userData;
|
||
|
|
if (!data->leaves_only || TCOD_bsp_is_leaf(node)) {
|
||
|
|
data->nodes->push_back(node);
|
||
|
|
}
|
||
|
|
return true; // Continue traversal
|
||
|
|
}
|
||
|
|
|
||
|
|
// Method: leaves() -> Iterator[BSPNode]
|
||
|
|
PyObject* PyBSP::leaves(PyBSPObject* self, PyObject* Py_UNUSED(args))
|
||
|
|
{
|
||
|
|
if (!self->root) {
|
||
|
|
PyErr_SetString(PyExc_RuntimeError, "BSP not initialized");
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create iterator object
|
||
|
|
PyBSPIterObject* iter = (PyBSPIterObject*)mcrfpydef::PyBSPIterType.tp_alloc(
|
||
|
|
&mcrfpydef::PyBSPIterType, 0);
|
||
|
|
if (!iter) {
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Collect all leaf nodes
|
||
|
|
iter->nodes = new std::vector<TCOD_bsp_t*>();
|
||
|
|
TraversalData data = {iter->nodes, true}; // leaves_only = true
|
||
|
|
TCOD_bsp_traverse_level_order(self->root, collect_callback, &data);
|
||
|
|
|
||
|
|
iter->index = 0;
|
||
|
|
iter->bsp_owner = (PyObject*)self;
|
||
|
|
Py_INCREF(self);
|
||
|
|
|
||
|
|
return (PyObject*)iter;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Method: traverse(order) -> Iterator[BSPNode]
|
||
|
|
PyObject* PyBSP::traverse(PyBSPObject* self, PyObject* args, PyObject* kwds)
|
||
|
|
{
|
||
|
|
static const char* keywords[] = {"order", nullptr};
|
||
|
|
PyObject* order_obj = nullptr;
|
||
|
|
|
||
|
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", const_cast<char**>(keywords),
|
||
|
|
&order_obj)) {
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!self->root) {
|
||
|
|
PyErr_SetString(PyExc_RuntimeError, "BSP not initialized");
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
int order;
|
||
|
|
if (!PyTraversal::from_arg(order_obj, &order)) {
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create iterator object
|
||
|
|
PyBSPIterObject* iter = (PyBSPIterObject*)mcrfpydef::PyBSPIterType.tp_alloc(
|
||
|
|
&mcrfpydef::PyBSPIterType, 0);
|
||
|
|
if (!iter) {
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Collect all nodes in the specified order
|
||
|
|
iter->nodes = new std::vector<TCOD_bsp_t*>();
|
||
|
|
TraversalData data = {iter->nodes, false}; // leaves_only = false
|
||
|
|
|
||
|
|
switch (order) {
|
||
|
|
case TRAVERSAL_PRE_ORDER:
|
||
|
|
TCOD_bsp_traverse_pre_order(self->root, collect_callback, &data);
|
||
|
|
break;
|
||
|
|
case TRAVERSAL_IN_ORDER:
|
||
|
|
TCOD_bsp_traverse_in_order(self->root, collect_callback, &data);
|
||
|
|
break;
|
||
|
|
case TRAVERSAL_POST_ORDER:
|
||
|
|
TCOD_bsp_traverse_post_order(self->root, collect_callback, &data);
|
||
|
|
break;
|
||
|
|
case TRAVERSAL_LEVEL_ORDER:
|
||
|
|
TCOD_bsp_traverse_level_order(self->root, collect_callback, &data);
|
||
|
|
break;
|
||
|
|
case TRAVERSAL_INVERTED_LEVEL_ORDER:
|
||
|
|
TCOD_bsp_traverse_inverted_level_order(self->root, collect_callback, &data);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
iter->index = 0;
|
||
|
|
iter->bsp_owner = (PyObject*)self;
|
||
|
|
Py_INCREF(self);
|
||
|
|
|
||
|
|
return (PyObject*)iter;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Method: find(pos) -> BSPNode | None
|
||
|
|
PyObject* PyBSP::find(PyBSPObject* self, PyObject* args, PyObject* kwds)
|
||
|
|
{
|
||
|
|
if (!self->root) {
|
||
|
|
PyErr_SetString(PyExc_RuntimeError, "BSP not initialized");
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
int x, y;
|
||
|
|
if (!PyPosition_ParseInt(args, kwds, &x, &y)) {
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
TCOD_bsp_t* found = TCOD_bsp_find_node(self->root, x, y);
|
||
|
|
if (!found) {
|
||
|
|
Py_RETURN_NONE;
|
||
|
|
}
|
||
|
|
|
||
|
|
return PyBSPNode::create(found, (PyObject*)self);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Method: to_heightmap(...) -> HeightMap
|
||
|
|
PyObject* PyBSP::to_heightmap(PyBSPObject* self, PyObject* args, PyObject* kwds)
|
||
|
|
{
|
||
|
|
static const char* keywords[] = {"size", "select", "shrink", "value", nullptr};
|
||
|
|
PyObject* size_obj = nullptr;
|
||
|
|
const char* select = "leaves";
|
||
|
|
int shrink = 0;
|
||
|
|
float value = 1.0f;
|
||
|
|
|
||
|
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|Osif", const_cast<char**>(keywords),
|
||
|
|
&size_obj, &select, &shrink, &value)) {
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!self->root) {
|
||
|
|
PyErr_SetString(PyExc_RuntimeError, "BSP not initialized");
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Determine output size
|
||
|
|
int width = self->root->w;
|
||
|
|
int height = self->root->h;
|
||
|
|
if (size_obj != nullptr && size_obj != Py_None) {
|
||
|
|
if (!PyTuple_Check(size_obj) || PyTuple_Size(size_obj) != 2) {
|
||
|
|
PyErr_SetString(PyExc_TypeError, "size must be (width, height) tuple");
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
width = (int)PyLong_AsLong(PyTuple_GetItem(size_obj, 0));
|
||
|
|
height = (int)PyLong_AsLong(PyTuple_GetItem(size_obj, 1));
|
||
|
|
if (PyErr_Occurred()) {
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (width <= 0 || height <= 0) {
|
||
|
|
PyErr_SetString(PyExc_ValueError, "size values must be positive");
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (shrink < 0) {
|
||
|
|
PyErr_SetString(PyExc_ValueError, "shrink must be non-negative");
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Determine which nodes to select
|
||
|
|
bool select_leaves = false;
|
||
|
|
bool select_internal = false;
|
||
|
|
if (strcmp(select, "leaves") == 0) {
|
||
|
|
select_leaves = true;
|
||
|
|
} else if (strcmp(select, "all") == 0) {
|
||
|
|
select_leaves = true;
|
||
|
|
select_internal = true;
|
||
|
|
} else if (strcmp(select, "internal") == 0) {
|
||
|
|
select_internal = true;
|
||
|
|
} else {
|
||
|
|
PyErr_Format(PyExc_ValueError,
|
||
|
|
"select must be 'leaves', 'all', or 'internal', got '%s'", select);
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create HeightMap
|
||
|
|
PyObject* heightmap_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "HeightMap");
|
||
|
|
if (!heightmap_type) {
|
||
|
|
PyErr_SetString(PyExc_RuntimeError, "HeightMap type not found");
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
PyObject* size_tuple = Py_BuildValue("(ii)", width, height);
|
||
|
|
PyObject* hmap_args = PyTuple_Pack(1, size_tuple);
|
||
|
|
Py_DECREF(size_tuple);
|
||
|
|
|
||
|
|
PyHeightMapObject* hmap = (PyHeightMapObject*)PyObject_Call(heightmap_type, hmap_args, nullptr);
|
||
|
|
Py_DECREF(hmap_args);
|
||
|
|
Py_DECREF(heightmap_type);
|
||
|
|
|
||
|
|
if (!hmap) {
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Fill selected nodes
|
||
|
|
struct FillData {
|
||
|
|
TCOD_heightmap_t* heightmap;
|
||
|
|
int shrink;
|
||
|
|
float value;
|
||
|
|
bool select_leaves;
|
||
|
|
bool select_internal;
|
||
|
|
int bsp_x, bsp_y; // BSP origin for offset calculation
|
||
|
|
int hmap_w, hmap_h;
|
||
|
|
};
|
||
|
|
|
||
|
|
FillData fill_data = {
|
||
|
|
hmap->heightmap,
|
||
|
|
shrink,
|
||
|
|
value,
|
||
|
|
select_leaves,
|
||
|
|
select_internal,
|
||
|
|
self->root->x,
|
||
|
|
self->root->y,
|
||
|
|
width,
|
||
|
|
height
|
||
|
|
};
|
||
|
|
|
||
|
|
TCOD_bsp_traverse_level_order(self->root, [](TCOD_bsp_t* node, void* userData) -> bool {
|
||
|
|
FillData* data = (FillData*)userData;
|
||
|
|
bool is_leaf = TCOD_bsp_is_leaf(node);
|
||
|
|
|
||
|
|
// Check if this node should be selected
|
||
|
|
if ((is_leaf && !data->select_leaves) || (!is_leaf && !data->select_internal)) {
|
||
|
|
return true; // Continue but don't fill
|
||
|
|
}
|
||
|
|
|
||
|
|
// Calculate bounds with shrink
|
||
|
|
int x1 = node->x - data->bsp_x + data->shrink;
|
||
|
|
int y1 = node->y - data->bsp_y + data->shrink;
|
||
|
|
int x2 = node->x - data->bsp_x + node->w - data->shrink;
|
||
|
|
int y2 = node->y - data->bsp_y + node->h - data->shrink;
|
||
|
|
|
||
|
|
// Clamp to heightmap bounds
|
||
|
|
if (x1 < 0) x1 = 0;
|
||
|
|
if (y1 < 0) y1 = 0;
|
||
|
|
if (x2 > data->hmap_w) x2 = data->hmap_w;
|
||
|
|
if (y2 > data->hmap_h) y2 = data->hmap_h;
|
||
|
|
|
||
|
|
// Fill the region
|
||
|
|
for (int y = y1; y < y2; y++) {
|
||
|
|
for (int x = x1; x < x2; x++) {
|
||
|
|
TCOD_heightmap_set_value(data->heightmap, x, y, data->value);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return true;
|
||
|
|
}, &fill_data);
|
||
|
|
|
||
|
|
return (PyObject*)hmap;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ==================== BSPNode Property Definitions ====================
|
||
|
|
|
||
|
|
PyGetSetDef PyBSPNode::getsetters[] = {
|
||
|
|
{"bounds", (getter)PyBSPNode::get_bounds, NULL,
|
||
|
|
MCRF_PROPERTY(bounds, "Node bounds as ((x, y), (w, h)). Read-only."), NULL},
|
||
|
|
{"level", (getter)PyBSPNode::get_level, NULL,
|
||
|
|
MCRF_PROPERTY(level, "Depth in tree (0 for root). Read-only."), NULL},
|
||
|
|
{"is_leaf", (getter)PyBSPNode::get_is_leaf, NULL,
|
||
|
|
MCRF_PROPERTY(is_leaf, "True if this node has no children. Read-only."), NULL},
|
||
|
|
{"split_horizontal", (getter)PyBSPNode::get_split_horizontal, NULL,
|
||
|
|
MCRF_PROPERTY(split_horizontal, "Split orientation, None if leaf. Read-only."), NULL},
|
||
|
|
{"split_position", (getter)PyBSPNode::get_split_position, NULL,
|
||
|
|
MCRF_PROPERTY(split_position, "Split coordinate, None if leaf. Read-only."), NULL},
|
||
|
|
{"left", (getter)PyBSPNode::get_left, NULL,
|
||
|
|
MCRF_PROPERTY(left, "Left child, or None if leaf. Read-only."), NULL},
|
||
|
|
{"right", (getter)PyBSPNode::get_right, NULL,
|
||
|
|
MCRF_PROPERTY(right, "Right child, or None if leaf. Read-only."), NULL},
|
||
|
|
{"parent", (getter)PyBSPNode::get_parent, NULL,
|
||
|
|
MCRF_PROPERTY(parent, "Parent node, or None if root. Read-only."), NULL},
|
||
|
|
{"sibling", (getter)PyBSPNode::get_sibling, NULL,
|
||
|
|
MCRF_PROPERTY(sibling, "Other child of parent, or None. Read-only."), NULL},
|
||
|
|
{NULL}
|
||
|
|
};
|
||
|
|
|
||
|
|
// ==================== BSPNode Method Definitions ====================
|
||
|
|
|
||
|
|
PyMethodDef PyBSPNode::methods[] = {
|
||
|
|
{"contains", (PyCFunction)PyBSPNode::contains, METH_VARARGS | METH_KEYWORDS,
|
||
|
|
MCRF_METHOD(BSPNode, contains,
|
||
|
|
MCRF_SIG("(pos: tuple[int, int])", "bool"),
|
||
|
|
MCRF_DESC("Check if position is inside this node's bounds."),
|
||
|
|
MCRF_ARGS_START
|
||
|
|
MCRF_ARG("pos", "Position as (x, y) tuple, list, or Vector")
|
||
|
|
MCRF_RETURNS("bool: True if position is inside bounds")
|
||
|
|
)},
|
||
|
|
{"center", (PyCFunction)PyBSPNode::center, METH_NOARGS,
|
||
|
|
MCRF_METHOD(BSPNode, center,
|
||
|
|
MCRF_SIG("()", "tuple[int, int]"),
|
||
|
|
MCRF_DESC("Return the center point of this node's bounds."),
|
||
|
|
MCRF_RETURNS("tuple[int, int]: Center position (x + w//2, y + h//2)")
|
||
|
|
)},
|
||
|
|
{NULL}
|
||
|
|
};
|
||
|
|
|
||
|
|
// ==================== BSPNode Implementation ====================
|
||
|
|
|
||
|
|
PyObject* PyBSPNode::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds)
|
||
|
|
{
|
||
|
|
// BSPNode cannot be directly instantiated
|
||
|
|
PyErr_SetString(PyExc_TypeError,
|
||
|
|
"BSPNode cannot be instantiated directly. Use BSP methods to get nodes.");
|
||
|
|
return NULL;
|
||
|
|
}
|
||
|
|
|
||
|
|
int PyBSPNode::init(PyBSPNodeObject* self, PyObject* args, PyObject* kwds)
|
||
|
|
{
|
||
|
|
// This should never be called since pynew returns NULL
|
||
|
|
PyErr_SetString(PyExc_TypeError, "BSPNode cannot be instantiated directly");
|
||
|
|
return -1;
|
||
|
|
}
|
||
|
|
|
||
|
|
void PyBSPNode::dealloc(PyBSPNodeObject* self)
|
||
|
|
{
|
||
|
|
// Don't delete the node - it's owned by the BSP tree
|
||
|
|
Py_XDECREF(self->bsp_owner);
|
||
|
|
Py_TYPE(self)->tp_free((PyObject*)self);
|
||
|
|
}
|
||
|
|
|
||
|
|
PyObject* PyBSPNode::repr(PyObject* obj)
|
||
|
|
{
|
||
|
|
PyBSPNodeObject* self = (PyBSPNodeObject*)obj;
|
||
|
|
std::ostringstream ss;
|
||
|
|
|
||
|
|
if (self->node) {
|
||
|
|
const char* type = TCOD_bsp_is_leaf(self->node) ? "leaf" : "split";
|
||
|
|
ss << "<BSPNode " << type << " at (" << self->node->x << ", " << self->node->y
|
||
|
|
<< ") size (" << self->node->w << " x " << self->node->h << ") level "
|
||
|
|
<< (int)self->node->level << ">";
|
||
|
|
} else {
|
||
|
|
ss << "<BSPNode (invalid)>";
|
||
|
|
}
|
||
|
|
|
||
|
|
return PyUnicode_FromString(ss.str().c_str());
|
||
|
|
}
|
||
|
|
|
||
|
|
// Helper to create a BSPNode wrapper
|
||
|
|
PyObject* PyBSPNode::create(TCOD_bsp_t* node, PyObject* bsp_owner)
|
||
|
|
{
|
||
|
|
if (!node) {
|
||
|
|
Py_RETURN_NONE;
|
||
|
|
}
|
||
|
|
|
||
|
|
PyBSPNodeObject* py_node = (PyBSPNodeObject*)mcrfpydef::PyBSPNodeType.tp_alloc(
|
||
|
|
&mcrfpydef::PyBSPNodeType, 0);
|
||
|
|
if (!py_node) {
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
py_node->node = node;
|
||
|
|
py_node->bsp_owner = bsp_owner;
|
||
|
|
Py_INCREF(bsp_owner);
|
||
|
|
|
||
|
|
return (PyObject*)py_node;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Property: bounds
|
||
|
|
PyObject* PyBSPNode::get_bounds(PyBSPNodeObject* self, void* closure)
|
||
|
|
{
|
||
|
|
if (!self->node) {
|
||
|
|
PyErr_SetString(PyExc_RuntimeError, "BSPNode is invalid");
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
return Py_BuildValue("((ii)(ii))",
|
||
|
|
self->node->x, self->node->y,
|
||
|
|
self->node->w, self->node->h);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Property: level
|
||
|
|
PyObject* PyBSPNode::get_level(PyBSPNodeObject* self, void* closure)
|
||
|
|
{
|
||
|
|
if (!self->node) {
|
||
|
|
PyErr_SetString(PyExc_RuntimeError, "BSPNode is invalid");
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
return PyLong_FromLong(self->node->level);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Property: is_leaf
|
||
|
|
PyObject* PyBSPNode::get_is_leaf(PyBSPNodeObject* self, void* closure)
|
||
|
|
{
|
||
|
|
if (!self->node) {
|
||
|
|
PyErr_SetString(PyExc_RuntimeError, "BSPNode is invalid");
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
return PyBool_FromLong(TCOD_bsp_is_leaf(self->node));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Property: split_horizontal
|
||
|
|
PyObject* PyBSPNode::get_split_horizontal(PyBSPNodeObject* self, void* closure)
|
||
|
|
{
|
||
|
|
if (!self->node) {
|
||
|
|
PyErr_SetString(PyExc_RuntimeError, "BSPNode is invalid");
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
if (TCOD_bsp_is_leaf(self->node)) {
|
||
|
|
Py_RETURN_NONE;
|
||
|
|
}
|
||
|
|
return PyBool_FromLong(self->node->horizontal);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Property: split_position
|
||
|
|
PyObject* PyBSPNode::get_split_position(PyBSPNodeObject* self, void* closure)
|
||
|
|
{
|
||
|
|
if (!self->node) {
|
||
|
|
PyErr_SetString(PyExc_RuntimeError, "BSPNode is invalid");
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
if (TCOD_bsp_is_leaf(self->node)) {
|
||
|
|
Py_RETURN_NONE;
|
||
|
|
}
|
||
|
|
return PyLong_FromLong(self->node->position);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Property: left
|
||
|
|
PyObject* PyBSPNode::get_left(PyBSPNodeObject* self, void* closure)
|
||
|
|
{
|
||
|
|
if (!self->node) {
|
||
|
|
PyErr_SetString(PyExc_RuntimeError, "BSPNode is invalid");
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
return PyBSPNode::create(TCOD_bsp_left(self->node), self->bsp_owner);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Property: right
|
||
|
|
PyObject* PyBSPNode::get_right(PyBSPNodeObject* self, void* closure)
|
||
|
|
{
|
||
|
|
if (!self->node) {
|
||
|
|
PyErr_SetString(PyExc_RuntimeError, "BSPNode is invalid");
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
return PyBSPNode::create(TCOD_bsp_right(self->node), self->bsp_owner);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Property: parent
|
||
|
|
PyObject* PyBSPNode::get_parent(PyBSPNodeObject* self, void* closure)
|
||
|
|
{
|
||
|
|
if (!self->node) {
|
||
|
|
PyErr_SetString(PyExc_RuntimeError, "BSPNode is invalid");
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
return PyBSPNode::create(TCOD_bsp_father(self->node), self->bsp_owner);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Property: sibling
|
||
|
|
PyObject* PyBSPNode::get_sibling(PyBSPNodeObject* self, void* closure)
|
||
|
|
{
|
||
|
|
if (!self->node) {
|
||
|
|
PyErr_SetString(PyExc_RuntimeError, "BSPNode is invalid");
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
TCOD_bsp_t* parent = TCOD_bsp_father(self->node);
|
||
|
|
if (!parent) {
|
||
|
|
Py_RETURN_NONE;
|
||
|
|
}
|
||
|
|
|
||
|
|
TCOD_bsp_t* left = TCOD_bsp_left(parent);
|
||
|
|
TCOD_bsp_t* right = TCOD_bsp_right(parent);
|
||
|
|
|
||
|
|
if (left == self->node) {
|
||
|
|
return PyBSPNode::create(right, self->bsp_owner);
|
||
|
|
} else {
|
||
|
|
return PyBSPNode::create(left, self->bsp_owner);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Method: contains(pos) -> bool
|
||
|
|
PyObject* PyBSPNode::contains(PyBSPNodeObject* self, PyObject* args, PyObject* kwds)
|
||
|
|
{
|
||
|
|
if (!self->node) {
|
||
|
|
PyErr_SetString(PyExc_RuntimeError, "BSPNode is invalid");
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
int x, y;
|
||
|
|
if (!PyPosition_ParseInt(args, kwds, &x, &y)) {
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
return PyBool_FromLong(TCOD_bsp_contains(self->node, x, y));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Method: center() -> (x, y)
|
||
|
|
PyObject* PyBSPNode::center(PyBSPNodeObject* self, PyObject* Py_UNUSED(args))
|
||
|
|
{
|
||
|
|
if (!self->node) {
|
||
|
|
PyErr_SetString(PyExc_RuntimeError, "BSPNode is invalid");
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
int cx = self->node->x + self->node->w / 2;
|
||
|
|
int cy = self->node->y + self->node->h / 2;
|
||
|
|
|
||
|
|
return Py_BuildValue("(ii)", cx, cy);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ==================== BSP Iterator Implementation ====================
|
||
|
|
|
||
|
|
void PyBSPIter::dealloc(PyBSPIterObject* self)
|
||
|
|
{
|
||
|
|
if (self->nodes) {
|
||
|
|
delete self->nodes;
|
||
|
|
self->nodes = nullptr;
|
||
|
|
}
|
||
|
|
Py_XDECREF(self->bsp_owner);
|
||
|
|
Py_TYPE(self)->tp_free((PyObject*)self);
|
||
|
|
}
|
||
|
|
|
||
|
|
PyObject* PyBSPIter::iter(PyObject* self)
|
||
|
|
{
|
||
|
|
Py_INCREF(self);
|
||
|
|
return self;
|
||
|
|
}
|
||
|
|
|
||
|
|
PyObject* PyBSPIter::next(PyBSPIterObject* self)
|
||
|
|
{
|
||
|
|
if (!self || !self->nodes) {
|
||
|
|
PyErr_SetString(PyExc_RuntimeError, "Iterator is invalid");
|
||
|
|
return NULL;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (self->index >= self->nodes->size()) {
|
||
|
|
PyErr_SetNone(PyExc_StopIteration);
|
||
|
|
return NULL;
|
||
|
|
}
|
||
|
|
|
||
|
|
TCOD_bsp_t* node = (*self->nodes)[self->index++];
|
||
|
|
return PyBSPNode::create(node, self->bsp_owner);
|
||
|
|
}
|
||
|
|
|
||
|
|
int PyBSPIter::init(PyBSPIterObject* self, PyObject* args, PyObject* kwds)
|
||
|
|
{
|
||
|
|
PyErr_SetString(PyExc_TypeError, "BSPIter cannot be instantiated directly");
|
||
|
|
return -1;
|
||
|
|
}
|
||
|
|
|
||
|
|
PyObject* PyBSPIter::repr(PyObject* obj)
|
||
|
|
{
|
||
|
|
PyBSPIterObject* self = (PyBSPIterObject*)obj;
|
||
|
|
std::ostringstream ss;
|
||
|
|
|
||
|
|
if (self->nodes) {
|
||
|
|
ss << "<BSPIter at " << self->index << "/" << self->nodes->size() << ">";
|
||
|
|
} else {
|
||
|
|
ss << "<BSPIter (exhausted)>";
|
||
|
|
}
|
||
|
|
|
||
|
|
return PyUnicode_FromString(ss.str().c_str());
|
||
|
|
}
|