Safety improvements: - Generation counter detects stale BSPNode references after clear()/split_recursive() - GRID_MAX validation prevents oversized BSP trees - Depth parameter capped at 16 to prevent resource exhaustion - Iterator checks generation to detect invalidation during mutation API improvements: - Changed constructor from bounds=((x,y),(w,h)) to pos=(x,y), size=(w,h) - Added pos and size properties alongside bounds - BSPNode __eq__ compares underlying pointers for identity - BSP __iter__ as shorthand for leaves() - BSP __len__ returns leaf count Tests: - Added tests for stale node detection, GRID_MAX validation, depth cap - Added tests for __len__, __iter__, and BSPNode equality Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
8699bba9e6
commit
6caf3dcd05
4 changed files with 442 additions and 118 deletions
308
src/PyBSP.cpp
308
src/PyBSP.cpp
|
|
@ -85,11 +85,10 @@ PyObject* PyTraversal::create_enum_class(PyObject* module) {
|
|||
return NULL;
|
||||
}
|
||||
|
||||
// Cache reference
|
||||
// Cache reference (borrowed by module after AddObject)
|
||||
traversal_enum_class = traversal_class;
|
||||
Py_INCREF(traversal_enum_class);
|
||||
|
||||
// Add to module
|
||||
// Add to module (steals reference)
|
||||
if (PyModule_AddObject(module, "Traversal", traversal_class) < 0) {
|
||||
Py_DECREF(traversal_class);
|
||||
traversal_enum_class = nullptr;
|
||||
|
|
@ -99,6 +98,12 @@ PyObject* PyTraversal::create_enum_class(PyObject* module) {
|
|||
return traversal_class;
|
||||
}
|
||||
|
||||
void PyTraversal::cleanup() {
|
||||
// The enum class is owned by the module after PyModule_AddObject
|
||||
// We just clear our pointer; the module handles cleanup
|
||||
traversal_enum_class = nullptr;
|
||||
}
|
||||
|
||||
int PyTraversal::from_arg(PyObject* arg, int* out_order) {
|
||||
// Accept None -> default to LEVEL_ORDER
|
||||
if (arg == NULL || arg == Py_None) {
|
||||
|
|
@ -182,29 +187,41 @@ int PyTraversal::from_arg(PyObject* arg, int* out_order) {
|
|||
PyGetSetDef PyBSP::getsetters[] = {
|
||||
{"bounds", (getter)PyBSP::get_bounds, NULL,
|
||||
MCRF_PROPERTY(bounds, "Root node bounds as ((x, y), (w, h)). Read-only."), NULL},
|
||||
{"pos", (getter)PyBSP::get_pos, NULL,
|
||||
MCRF_PROPERTY(pos, "Top-left position (x, y). Read-only."), NULL},
|
||||
{"size", (getter)PyBSP::get_size, NULL,
|
||||
MCRF_PROPERTY(size, "Dimensions (width, height). Read-only."), NULL},
|
||||
{"root", (getter)PyBSP::get_root, NULL,
|
||||
MCRF_PROPERTY(root, "Reference to the root BSPNode. Read-only."), NULL},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
// Sequence methods for len() and iteration
|
||||
PySequenceMethods PyBSP::sequence_methods = {
|
||||
.sq_length = (lenfunc)PyBSP::len,
|
||||
};
|
||||
|
||||
// ==================== 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_DESC("Split the root node once at the specified position. "
|
||||
"horizontal=True creates a horizontal divider, producing top/bottom rooms. "
|
||||
"horizontal=False creates a vertical divider, producing left/right rooms."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("horizontal", "True for horizontal split, False for vertical")
|
||||
MCRF_ARG("horizontal", "True for horizontal divider (top/bottom), False for vertical (left/right)")
|
||||
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_DESC("Recursively split to the specified depth. "
|
||||
"WARNING: Invalidates all existing BSPNode references from this tree."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("depth", "Maximum recursion depth. Creates up to 2^depth leaves.")
|
||||
MCRF_ARG("depth", "Maximum recursion depth (1-16). 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.")
|
||||
|
|
@ -213,13 +230,14 @@ PyMethodDef PyBSP::methods[] = {
|
|||
{"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_DESC("Remove all children, keeping only the root node with original bounds. "
|
||||
"WARNING: Invalidates all existing BSPNode references from this tree."),
|
||||
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_DESC("Iterate all leaf nodes (the actual rooms). Same as iterating the BSP directly."),
|
||||
MCRF_RETURNS("Iterator yielding BSPNode objects")
|
||||
)},
|
||||
{"traverse", (PyCFunction)PyBSP::traverse, METH_VARARGS | METH_KEYWORDS,
|
||||
|
|
@ -264,41 +282,33 @@ PyObject* PyBSP::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds)
|
|||
self->orig_y = 0;
|
||||
self->orig_w = 0;
|
||||
self->orig_h = 0;
|
||||
self->generation = 0;
|
||||
}
|
||||
return (PyObject*)self;
|
||||
}
|
||||
|
||||
int PyBSP::init(PyBSPObject* self, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
static const char* keywords[] = {"bounds", nullptr};
|
||||
PyObject* bounds_obj = nullptr;
|
||||
static const char* keywords[] = {"pos", "size", nullptr};
|
||||
PyObject* pos_obj = nullptr;
|
||||
PyObject* size_obj = nullptr;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", const_cast<char**>(keywords),
|
||||
&bounds_obj)) {
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO", const_cast<char**>(keywords),
|
||||
&pos_obj, &size_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");
|
||||
// Parse position using PyPositionHelper pattern
|
||||
int x, y;
|
||||
if (!PyPosition_FromObjectInt(pos_obj, &x, &y)) {
|
||||
PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y), list, or Vector");
|
||||
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()) {
|
||||
// Parse size using PyPositionHelper pattern
|
||||
int w, h;
|
||||
if (!PyPosition_FromObjectInt(size_obj, &w, &h)) {
|
||||
PyErr_SetString(PyExc_TypeError, "size must be a tuple (w, h), list, or Vector");
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
|
@ -307,6 +317,14 @@ int PyBSP::init(PyBSPObject* self, PyObject* args, PyObject* kwds)
|
|||
return -1;
|
||||
}
|
||||
|
||||
// Validate against GRID_MAX like HeightMap does
|
||||
if (w > GRID_MAX || h > GRID_MAX) {
|
||||
PyErr_Format(PyExc_ValueError,
|
||||
"BSP dimensions cannot exceed %d (got %dx%d)",
|
||||
GRID_MAX, w, h);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Clean up any existing BSP
|
||||
if (self->root) {
|
||||
TCOD_bsp_delete(self->root);
|
||||
|
|
@ -324,6 +342,7 @@ int PyBSP::init(PyBSPObject* self, PyObject* args, PyObject* kwds)
|
|||
self->orig_y = y;
|
||||
self->orig_w = w;
|
||||
self->orig_h = h;
|
||||
self->generation = 0;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -373,6 +392,26 @@ PyObject* PyBSP::get_bounds(PyBSPObject* self, void* closure)
|
|||
self->root->w, self->root->h);
|
||||
}
|
||||
|
||||
// Property: pos
|
||||
PyObject* PyBSP::get_pos(PyBSPObject* self, void* closure)
|
||||
{
|
||||
if (!self->root) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "BSP not initialized");
|
||||
return nullptr;
|
||||
}
|
||||
return Py_BuildValue("(ii)", self->root->x, self->root->y);
|
||||
}
|
||||
|
||||
// Property: size
|
||||
PyObject* PyBSP::get_size(PyBSPObject* self, void* closure)
|
||||
{
|
||||
if (!self->root) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "BSP not initialized");
|
||||
return nullptr;
|
||||
}
|
||||
return Py_BuildValue("(ii)", self->root->w, self->root->h);
|
||||
}
|
||||
|
||||
// Property: root
|
||||
PyObject* PyBSP::get_root(PyBSPObject* self, void* closure)
|
||||
{
|
||||
|
|
@ -400,6 +439,8 @@ PyObject* PyBSP::split_once(PyBSPObject* self, PyObject* args, PyObject* kwds)
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
// Note: split_once only adds children, doesn't remove any nodes
|
||||
// Root node pointer remains valid, so we don't increment generation
|
||||
TCOD_bsp_split_once(self->root, horizontal ? true : false, position);
|
||||
|
||||
Py_INCREF(self);
|
||||
|
|
@ -425,21 +466,22 @@ PyObject* PyBSP::split_recursive(PyBSPObject* self, PyObject* args, PyObject* kw
|
|||
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");
|
||||
// Parse min_size using PyPositionHelper pattern
|
||||
int min_w, min_h;
|
||||
if (!PyPosition_FromObjectInt(min_size_obj, &min_w, &min_h)) {
|
||||
PyErr_SetString(PyExc_TypeError, "min_size must be (width, height) tuple, list, or Vector");
|
||||
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()) {
|
||||
if (depth < 1) {
|
||||
PyErr_SetString(PyExc_ValueError, "depth must be at least 1");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (depth < 0) {
|
||||
PyErr_SetString(PyExc_ValueError, "depth must be non-negative");
|
||||
if (depth > BSP_MAX_DEPTH) {
|
||||
PyErr_Format(PyExc_ValueError,
|
||||
"depth cannot exceed %d (got %d) to prevent memory exhaustion",
|
||||
BSP_MAX_DEPTH, depth);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
|
@ -462,6 +504,9 @@ PyObject* PyBSP::split_recursive(PyBSPObject* self, PyObject* args, PyObject* kw
|
|||
rnd = TCOD_random_new_from_seed(TCOD_RNG_MT, seed);
|
||||
}
|
||||
|
||||
// Increment generation BEFORE splitting - invalidates existing nodes
|
||||
self->generation++;
|
||||
|
||||
TCOD_bsp_split_recursive(self->root, rnd, depth, min_w, min_h, max_ratio, max_ratio);
|
||||
|
||||
if (rnd) {
|
||||
|
|
@ -480,6 +525,9 @@ PyObject* PyBSP::clear(PyBSPObject* self, PyObject* Py_UNUSED(args))
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
// Increment generation BEFORE clearing - invalidates existing nodes
|
||||
self->generation++;
|
||||
|
||||
TCOD_bsp_remove_sons(self->root);
|
||||
|
||||
// Restore original bounds
|
||||
|
|
@ -489,6 +537,31 @@ PyObject* PyBSP::clear(PyBSPObject* self, PyObject* Py_UNUSED(args))
|
|||
return (PyObject*)self;
|
||||
}
|
||||
|
||||
// Sequence protocol: len(bsp) returns leaf count
|
||||
Py_ssize_t PyBSP::len(PyBSPObject* self)
|
||||
{
|
||||
if (!self->root) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "BSP not initialized");
|
||||
return -1;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
return (Py_ssize_t)leaf_count;
|
||||
}
|
||||
|
||||
// __iter__ is shorthand for leaves()
|
||||
PyObject* PyBSP::iter(PyBSPObject* self)
|
||||
{
|
||||
return PyBSP::leaves(self, nullptr);
|
||||
}
|
||||
|
||||
// ==================== BSP Iteration ====================
|
||||
|
||||
// Traversal callback to collect nodes
|
||||
|
|
@ -527,6 +600,7 @@ PyObject* PyBSP::leaves(PyBSPObject* self, PyObject* Py_UNUSED(args))
|
|||
|
||||
iter->index = 0;
|
||||
iter->bsp_owner = (PyObject*)self;
|
||||
iter->generation = self->generation; // Capture generation for validity check
|
||||
Py_INCREF(self);
|
||||
|
||||
return (PyObject*)iter;
|
||||
|
|
@ -584,6 +658,7 @@ PyObject* PyBSP::traverse(PyBSPObject* self, PyObject* args, PyObject* kwds)
|
|||
|
||||
iter->index = 0;
|
||||
iter->bsp_owner = (PyObject*)self;
|
||||
iter->generation = self->generation; // Capture generation for validity check
|
||||
Py_INCREF(self);
|
||||
|
||||
return (PyObject*)iter;
|
||||
|
|
@ -633,13 +708,8 @@ PyObject* PyBSP::to_heightmap(PyBSPObject* self, PyObject* args, PyObject* kwds)
|
|||
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()) {
|
||||
if (!PyPosition_FromObjectInt(size_obj, &width, &height)) {
|
||||
PyErr_SetString(PyExc_TypeError, "size must be (width, height) tuple, list, or Vector");
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
|
@ -751,6 +821,10 @@ PyObject* PyBSP::to_heightmap(PyBSPObject* self, PyObject* args, PyObject* kwds)
|
|||
PyGetSetDef PyBSPNode::getsetters[] = {
|
||||
{"bounds", (getter)PyBSPNode::get_bounds, NULL,
|
||||
MCRF_PROPERTY(bounds, "Node bounds as ((x, y), (w, h)). Read-only."), NULL},
|
||||
{"pos", (getter)PyBSPNode::get_pos, NULL,
|
||||
MCRF_PROPERTY(pos, "Top-left position (x, y). Read-only."), NULL},
|
||||
{"size", (getter)PyBSPNode::get_size, NULL,
|
||||
MCRF_PROPERTY(size, "Dimensions (width, height). 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,
|
||||
|
|
@ -792,6 +866,30 @@ PyMethodDef PyBSPNode::methods[] = {
|
|||
|
||||
// ==================== BSPNode Implementation ====================
|
||||
|
||||
// Validity check - returns false and sets error if node is stale
|
||||
bool PyBSPNode::checkValid(PyBSPNodeObject* self)
|
||||
{
|
||||
if (!self->node) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "BSPNode is invalid (null pointer)");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!self->bsp_owner) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "BSPNode has no parent BSP");
|
||||
return false;
|
||||
}
|
||||
|
||||
PyBSPObject* bsp = (PyBSPObject*)self->bsp_owner;
|
||||
if (self->generation != bsp->generation) {
|
||||
PyErr_SetString(PyExc_RuntimeError,
|
||||
"BSPNode is stale: parent BSP was modified (clear() or split_recursive() called). "
|
||||
"Re-fetch nodes from the BSP object.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
PyObject* PyBSPNode::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
// BSPNode cannot be directly instantiated
|
||||
|
|
@ -817,20 +915,52 @@ void PyBSPNode::dealloc(PyBSPNodeObject* self)
|
|||
PyObject* PyBSPNode::repr(PyObject* obj)
|
||||
{
|
||||
PyBSPNodeObject* self = (PyBSPNodeObject*)obj;
|
||||
|
||||
// Check validity without raising error for repr
|
||||
PyBSPObject* bsp = self->bsp_owner ? (PyBSPObject*)self->bsp_owner : nullptr;
|
||||
bool is_valid = self->node && bsp && self->generation == bsp->generation;
|
||||
|
||||
std::ostringstream ss;
|
||||
|
||||
if (self->node) {
|
||||
if (is_valid) {
|
||||
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)>";
|
||||
ss << "<BSPNode (invalid/stale)>";
|
||||
}
|
||||
|
||||
return PyUnicode_FromString(ss.str().c_str());
|
||||
}
|
||||
|
||||
// Rich comparison for BSPNode - compares underlying pointers
|
||||
PyObject* PyBSPNode::richcompare(PyObject* self, PyObject* other, int op)
|
||||
{
|
||||
// Only support == and !=
|
||||
if (op != Py_EQ && op != Py_NE) {
|
||||
Py_RETURN_NOTIMPLEMENTED;
|
||||
}
|
||||
|
||||
// Check if other is also a BSPNode
|
||||
if (!PyObject_TypeCheck(other, &mcrfpydef::PyBSPNodeType)) {
|
||||
if (op == Py_EQ) Py_RETURN_FALSE;
|
||||
else Py_RETURN_TRUE;
|
||||
}
|
||||
|
||||
PyBSPNodeObject* self_node = (PyBSPNodeObject*)self;
|
||||
PyBSPNodeObject* other_node = (PyBSPNodeObject*)other;
|
||||
|
||||
// Compare wrapped pointers
|
||||
bool equal = (self_node->node == other_node->node);
|
||||
|
||||
if (op == Py_EQ) {
|
||||
return PyBool_FromLong(equal);
|
||||
} else {
|
||||
return PyBool_FromLong(!equal);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to create a BSPNode wrapper
|
||||
PyObject* PyBSPNode::create(TCOD_bsp_t* node, PyObject* bsp_owner)
|
||||
{
|
||||
|
|
@ -846,6 +976,7 @@ PyObject* PyBSPNode::create(TCOD_bsp_t* node, PyObject* bsp_owner)
|
|||
|
||||
py_node->node = node;
|
||||
py_node->bsp_owner = bsp_owner;
|
||||
py_node->generation = ((PyBSPObject*)bsp_owner)->generation;
|
||||
Py_INCREF(bsp_owner);
|
||||
|
||||
return (PyObject*)py_node;
|
||||
|
|
@ -854,42 +985,44 @@ PyObject* PyBSPNode::create(TCOD_bsp_t* node, PyObject* bsp_owner)
|
|||
// Property: bounds
|
||||
PyObject* PyBSPNode::get_bounds(PyBSPNodeObject* self, void* closure)
|
||||
{
|
||||
if (!self->node) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "BSPNode is invalid");
|
||||
return nullptr;
|
||||
}
|
||||
if (!checkValid(self)) return nullptr;
|
||||
return Py_BuildValue("((ii)(ii))",
|
||||
self->node->x, self->node->y,
|
||||
self->node->w, self->node->h);
|
||||
}
|
||||
|
||||
// Property: pos
|
||||
PyObject* PyBSPNode::get_pos(PyBSPNodeObject* self, void* closure)
|
||||
{
|
||||
if (!checkValid(self)) return nullptr;
|
||||
return Py_BuildValue("(ii)", self->node->x, self->node->y);
|
||||
}
|
||||
|
||||
// Property: size
|
||||
PyObject* PyBSPNode::get_size(PyBSPNodeObject* self, void* closure)
|
||||
{
|
||||
if (!checkValid(self)) return nullptr;
|
||||
return Py_BuildValue("(ii)", 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;
|
||||
}
|
||||
if (!checkValid(self)) 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;
|
||||
}
|
||||
if (!checkValid(self)) 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 (!checkValid(self)) return nullptr;
|
||||
if (TCOD_bsp_is_leaf(self->node)) {
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
|
@ -899,10 +1032,7 @@ PyObject* PyBSPNode::get_split_horizontal(PyBSPNodeObject* self, void* closure)
|
|||
// 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 (!checkValid(self)) return nullptr;
|
||||
if (TCOD_bsp_is_leaf(self->node)) {
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
|
@ -912,40 +1042,28 @@ PyObject* PyBSPNode::get_split_position(PyBSPNodeObject* self, void* closure)
|
|||
// Property: left
|
||||
PyObject* PyBSPNode::get_left(PyBSPNodeObject* self, void* closure)
|
||||
{
|
||||
if (!self->node) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "BSPNode is invalid");
|
||||
return nullptr;
|
||||
}
|
||||
if (!checkValid(self)) 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;
|
||||
}
|
||||
if (!checkValid(self)) 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;
|
||||
}
|
||||
if (!checkValid(self)) 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;
|
||||
}
|
||||
if (!checkValid(self)) return nullptr;
|
||||
|
||||
TCOD_bsp_t* parent = TCOD_bsp_father(self->node);
|
||||
if (!parent) {
|
||||
|
|
@ -965,10 +1083,7 @@ PyObject* PyBSPNode::get_sibling(PyBSPNodeObject* self, void* closure)
|
|||
// 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;
|
||||
}
|
||||
if (!checkValid(self)) return nullptr;
|
||||
|
||||
int x, y;
|
||||
if (!PyPosition_ParseInt(args, kwds, &x, &y)) {
|
||||
|
|
@ -981,10 +1096,7 @@ PyObject* PyBSPNode::contains(PyBSPNodeObject* self, PyObject* args, PyObject* k
|
|||
// 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;
|
||||
}
|
||||
if (!checkValid(self)) return nullptr;
|
||||
|
||||
int cx = self->node->x + self->node->w / 2;
|
||||
int cy = self->node->y + self->node->h / 2;
|
||||
|
|
@ -1017,6 +1129,16 @@ PyObject* PyBSPIter::next(PyBSPIterObject* self)
|
|||
return NULL;
|
||||
}
|
||||
|
||||
// Check for tree modification during iteration
|
||||
if (self->bsp_owner) {
|
||||
PyBSPObject* bsp = (PyBSPObject*)self->bsp_owner;
|
||||
if (self->generation != bsp->generation) {
|
||||
PyErr_SetString(PyExc_RuntimeError,
|
||||
"BSP tree was modified during iteration (clear() or split_recursive() called)");
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
if (self->index >= self->nodes->size()) {
|
||||
PyErr_SetNone(PyExc_StopIteration);
|
||||
return NULL;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue