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
53
src/PyBSP.h
53
src/PyBSP.h
|
|
@ -3,17 +3,23 @@
|
|||
#include "Python.h"
|
||||
#include <libtcod.h>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
|
||||
// Forward declarations
|
||||
class PyBSP;
|
||||
class PyBSPNode;
|
||||
|
||||
// Maximum recursion depth to prevent memory exhaustion
|
||||
// 2^16 = 65536 potential leaf nodes, which is already excessive
|
||||
constexpr int BSP_MAX_DEPTH = 16;
|
||||
|
||||
// Python object structure for BSP tree (root owner)
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
TCOD_bsp_t* root; // libtcod BSP root (owned, will be deleted)
|
||||
int orig_x, orig_y; // Original bounds for clear()
|
||||
int orig_w, orig_h;
|
||||
uint64_t generation; // Incremented on structural changes (clear, split)
|
||||
} PyBSPObject;
|
||||
|
||||
// Python object structure for BSPNode (lightweight reference)
|
||||
|
|
@ -21,6 +27,7 @@ typedef struct {
|
|||
PyObject_HEAD
|
||||
TCOD_bsp_t* node; // libtcod BSP node (NOT owned)
|
||||
PyObject* bsp_owner; // Reference to PyBSPObject to prevent dangling
|
||||
uint64_t generation; // Generation at time of creation (for validity check)
|
||||
} PyBSPNodeObject;
|
||||
|
||||
// BSP iterator for traverse()
|
||||
|
|
@ -29,6 +36,7 @@ typedef struct {
|
|||
std::vector<TCOD_bsp_t*>* nodes; // Pre-collected nodes
|
||||
size_t index;
|
||||
PyObject* bsp_owner; // Reference to PyBSPObject
|
||||
uint64_t generation; // Generation at iterator creation
|
||||
} PyBSPIterObject;
|
||||
|
||||
class PyBSP
|
||||
|
|
@ -42,6 +50,8 @@ public:
|
|||
|
||||
// Properties
|
||||
static PyObject* get_bounds(PyBSPObject* self, void* closure);
|
||||
static PyObject* get_pos(PyBSPObject* self, void* closure);
|
||||
static PyObject* get_size(PyBSPObject* self, void* closure);
|
||||
static PyObject* get_root(PyBSPObject* self, void* closure);
|
||||
|
||||
// Splitting methods (#202)
|
||||
|
|
@ -59,9 +69,14 @@ public:
|
|||
// HeightMap conversion (#206)
|
||||
static PyObject* to_heightmap(PyBSPObject* self, PyObject* args, PyObject* kwds);
|
||||
|
||||
// Sequence protocol
|
||||
static Py_ssize_t len(PyBSPObject* self);
|
||||
static PyObject* iter(PyBSPObject* self);
|
||||
|
||||
// Method and property definitions
|
||||
static PyMethodDef methods[];
|
||||
static PyGetSetDef getsetters[];
|
||||
static PySequenceMethods sequence_methods;
|
||||
};
|
||||
|
||||
class PyBSPNode
|
||||
|
|
@ -73,8 +88,13 @@ public:
|
|||
static void dealloc(PyBSPNodeObject* self);
|
||||
static PyObject* repr(PyObject* obj);
|
||||
|
||||
// Comparison
|
||||
static PyObject* richcompare(PyObject* self, PyObject* other, int op);
|
||||
|
||||
// Properties (#203)
|
||||
static PyObject* get_bounds(PyBSPNodeObject* self, void* closure);
|
||||
static PyObject* get_pos(PyBSPNodeObject* self, void* closure);
|
||||
static PyObject* get_size(PyBSPNodeObject* self, void* closure);
|
||||
static PyObject* get_level(PyBSPNodeObject* self, void* closure);
|
||||
static PyObject* get_is_leaf(PyBSPNodeObject* self, void* closure);
|
||||
static PyObject* get_split_horizontal(PyBSPNodeObject* self, void* closure);
|
||||
|
|
@ -93,6 +113,9 @@ public:
|
|||
// Helper to create a BSPNode from a TCOD_bsp_t*
|
||||
static PyObject* create(TCOD_bsp_t* node, PyObject* bsp_owner);
|
||||
|
||||
// Validity check - returns false and sets error if node is stale
|
||||
static bool checkValid(PyBSPNodeObject* self);
|
||||
|
||||
// Method and property definitions
|
||||
static PyMethodDef methods[];
|
||||
static PyGetSetDef getsetters[];
|
||||
|
|
@ -116,6 +139,8 @@ public:
|
|||
static PyObject* traversal_enum_class;
|
||||
static PyObject* create_enum_class(PyObject* module);
|
||||
static int from_arg(PyObject* arg, int* out_order);
|
||||
// Cleanup for module finalization (optional)
|
||||
static void cleanup();
|
||||
};
|
||||
|
||||
namespace mcrfpydef {
|
||||
|
|
@ -127,23 +152,31 @@ namespace mcrfpydef {
|
|||
.tp_itemsize = 0,
|
||||
.tp_dealloc = (destructor)PyBSP::dealloc,
|
||||
.tp_repr = PyBSP::repr,
|
||||
.tp_as_sequence = &PyBSP::sequence_methods,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = PyDoc_STR(
|
||||
"BSP(bounds: tuple[tuple[int, int], tuple[int, int]])\n\n"
|
||||
"BSP(pos: tuple[int, int], size: tuple[int, int])\n\n"
|
||||
"Binary Space Partitioning tree for procedural dungeon generation.\n\n"
|
||||
"BSP recursively divides a rectangular region into smaller sub-regions, "
|
||||
"creating a tree structure perfect for generating dungeon rooms and corridors.\n\n"
|
||||
"Args:\n"
|
||||
" bounds: ((x, y), (w, h)) - Position and size of the root region.\n\n"
|
||||
" pos: (x, y) - Top-left position of the root region.\n"
|
||||
" size: (w, h) - Width and height of the root region.\n\n"
|
||||
"Properties:\n"
|
||||
" bounds ((x, y), (w, h)): Read-only. Root node bounds.\n"
|
||||
" pos (tuple[int, int]): Read-only. Top-left position (x, y).\n"
|
||||
" size (tuple[int, int]): Read-only. Dimensions (width, height).\n"
|
||||
" bounds ((pos), (size)): Read-only. Combined position and size.\n"
|
||||
" root (BSPNode): Read-only. Reference to the root node.\n\n"
|
||||
"Iteration:\n"
|
||||
" for leaf in bsp: # Iterates over leaf nodes (rooms)\n"
|
||||
" len(bsp) # Returns number of leaf nodes\n\n"
|
||||
"Example:\n"
|
||||
" bsp = mcrfpy.BSP(bounds=((0, 0), (80, 50)))\n"
|
||||
" bsp = mcrfpy.BSP(pos=(0, 0), size=(80, 50))\n"
|
||||
" bsp.split_recursive(depth=4, min_size=(8, 8))\n"
|
||||
" for leaf in bsp.leaves():\n"
|
||||
" print(f'Room at {leaf.bounds}')\n"
|
||||
" for leaf in bsp:\n"
|
||||
" print(f'Room at {leaf.pos}, size {leaf.size}')\n"
|
||||
),
|
||||
.tp_iter = (getiterfunc)PyBSP::iter,
|
||||
.tp_methods = nullptr, // Set in McRFPy_API.cpp
|
||||
.tp_getset = nullptr, // Set in McRFPy_API.cpp
|
||||
.tp_init = (initproc)PyBSP::init,
|
||||
|
|
@ -163,8 +196,13 @@ namespace mcrfpydef {
|
|||
"BSPNode - Lightweight reference to a node in a BSP tree.\n\n"
|
||||
"BSPNode provides read-only access to node properties and navigation.\n"
|
||||
"Nodes are created by BSP methods, not directly instantiated.\n\n"
|
||||
"WARNING: BSPNode references become invalid after BSP.clear() or\n"
|
||||
"BSP.split_recursive(). Accessing properties of an invalid node\n"
|
||||
"raises RuntimeError.\n\n"
|
||||
"Properties:\n"
|
||||
" bounds ((x, y), (w, h)): Position and size of this node.\n"
|
||||
" pos (tuple[int, int]): Top-left position (x, y).\n"
|
||||
" size (tuple[int, int]): Dimensions (width, height).\n"
|
||||
" bounds ((pos), (size)): Combined position and size.\n"
|
||||
" level (int): Depth in tree (0 for root).\n"
|
||||
" is_leaf (bool): True if this node has no children.\n"
|
||||
" split_horizontal (bool | None): Split orientation, None if leaf.\n"
|
||||
|
|
@ -174,6 +212,7 @@ namespace mcrfpydef {
|
|||
" parent (BSPNode | None): Parent node, or None if root.\n"
|
||||
" sibling (BSPNode | None): Other child of parent, or None.\n"
|
||||
),
|
||||
.tp_richcompare = PyBSPNode::richcompare,
|
||||
.tp_methods = nullptr, // Set in McRFPy_API.cpp
|
||||
.tp_getset = nullptr, // Set in McRFPy_API.cpp
|
||||
.tp_init = (initproc)PyBSPNode::init,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue