diff --git a/src/GridLayers.h b/src/GridLayers.h index 290162d..02a5e43 100644 --- a/src/GridLayers.h +++ b/src/GridLayers.h @@ -253,33 +253,18 @@ namespace mcrfpydef { .tp_repr = (reprfunc)PyGridLayerAPI::ColorLayer_repr, .tp_flags = Py_TPFLAGS_DEFAULT, .tp_doc = PyDoc_STR("ColorLayer(z_index=-1, grid_size=None)\n\n" - "A grid layer that stores RGBA colors per cell for background/overlay effects.\n\n" - "ColorLayers are typically created via Grid.add_layer('color', ...) rather than\n" - "instantiated directly. When attached to a Grid, the layer inherits rendering\n" - "parameters and can participate in FOV (field of view) calculations.\n\n" + "A grid layer that stores RGBA colors per cell.\n\n" "Args:\n" - " z_index (int): Render order relative to entities. Negative values render\n" - " below entities (as backgrounds), positive values render above entities\n" - " (as overlays). Default: -1 (background)\n" - " grid_size (tuple): Dimensions as (width, height). If None, the layer will\n" - " inherit the parent Grid's dimensions when attached. Default: None\n\n" + " z_index (int): Render order. Negative = below entities. Default: -1\n" + " grid_size (tuple): Dimensions as (width, height). Default: parent grid size\n\n" "Attributes:\n" - " z_index (int): Layer z-order relative to entities (read/write)\n" - " visible (bool): Whether layer is rendered (read/write)\n" - " grid_size (tuple): Layer dimensions as (width, height) (read-only)\n\n" + " z_index (int): Layer z-order relative to entities\n" + " visible (bool): Whether layer is rendered\n" + " grid_size (tuple): Layer dimensions (read-only)\n\n" "Methods:\n" - " at(x, y) -> Color: Get the color at cell position (x, y)\n" - " set(x, y, color): Set the color at cell position (x, y)\n" - " fill(color): Fill the entire layer with a single color\n" - " fill_rect(x, y, w, h, color): Fill a rectangular region with a color\n" - " draw_fov(...): Draw FOV-based visibility colors\n" - " apply_perspective(entity, ...): Bind layer to entity for automatic FOV updates\n\n" - "Example:\n" - " grid = mcrfpy.Grid(grid_size=(20, 15), texture=my_texture,\n" - " pos=(50, 50), size=(640, 480))\n" - " layer = grid.add_layer('color', z_index=-1)\n" - " layer.fill(mcrfpy.Color(40, 40, 40)) # Dark gray background\n" - " layer.set(5, 5, mcrfpy.Color(255, 0, 0, 128)) # Semi-transparent red cell"), + " at(x, y): Get color at cell position\n" + " set(x, y, color): Set color at cell position\n" + " fill(color): Fill entire layer with color"), .tp_methods = PyGridLayerAPI::ColorLayer_methods, .tp_getset = PyGridLayerAPI::ColorLayer_getsetters, .tp_init = (initproc)PyGridLayerAPI::ColorLayer_init, @@ -304,39 +289,20 @@ namespace mcrfpydef { .tp_repr = (reprfunc)PyGridLayerAPI::TileLayer_repr, .tp_flags = Py_TPFLAGS_DEFAULT, .tp_doc = PyDoc_STR("TileLayer(z_index=-1, texture=None, grid_size=None)\n\n" - "A grid layer that stores sprite indices per cell for tile-based rendering.\n\n" - "TileLayers are typically created via Grid.add_layer('tile', ...) rather than\n" - "instantiated directly. Each cell stores an integer index into the layer's\n" - "sprite atlas texture. An index of -1 means no tile (transparent/empty).\n\n" + "A grid layer that stores sprite indices per cell.\n\n" "Args:\n" - " z_index (int): Render order relative to entities. Negative values render\n" - " below entities (as backgrounds), positive values render above entities\n" - " (as overlays). Default: -1 (background)\n" - " texture (Texture): Sprite atlas containing tile images. The texture's\n" - " sprite_size determines individual tile dimensions. Required for\n" - " rendering; can be set after creation. Default: None\n" - " grid_size (tuple): Dimensions as (width, height). If None, the layer will\n" - " inherit the parent Grid's dimensions when attached. Default: None\n\n" + " z_index (int): Render order. Negative = below entities. Default: -1\n" + " texture (Texture): Sprite atlas for tile rendering. Default: None\n" + " grid_size (tuple): Dimensions as (width, height). Default: parent grid size\n\n" "Attributes:\n" - " z_index (int): Layer z-order relative to entities (read/write)\n" - " visible (bool): Whether layer is rendered (read/write)\n" - " texture (Texture): Sprite atlas for tile images (read/write)\n" - " grid_size (tuple): Layer dimensions as (width, height) (read-only)\n\n" + " z_index (int): Layer z-order relative to entities\n" + " visible (bool): Whether layer is rendered\n" + " texture (Texture): Tile sprite atlas\n" + " grid_size (tuple): Layer dimensions (read-only)\n\n" "Methods:\n" - " at(x, y) -> int: Get the tile index at cell position (x, y)\n" - " set(x, y, index): Set the tile index at cell position (x, y)\n" - " fill(index): Fill the entire layer with a single tile index\n" - " fill_rect(x, y, w, h, index): Fill a rectangular region with a tile index\n\n" - "Tile Index Values:\n" - " -1: No tile (transparent/empty cell)\n" - " 0+: Index into the texture's sprite atlas (row-major order)\n\n" - "Example:\n" - " grid = mcrfpy.Grid(grid_size=(20, 15), texture=my_texture,\n" - " pos=(50, 50), size=(640, 480))\n" - " layer = grid.add_layer('tile', z_index=1, texture=overlay_texture)\n" - " layer.fill(-1) # Clear layer (all transparent)\n" - " layer.set(5, 5, 42) # Place tile index 42 at position (5, 5)\n" - " layer.fill_rect(0, 0, 20, 1, 10) # Top row filled with tile 10"), + " at(x, y): Get tile index at cell position\n" + " set(x, y, index): Set tile index at cell position\n" + " fill(index): Fill entire layer with tile index"), .tp_methods = PyGridLayerAPI::TileLayer_methods, .tp_getset = PyGridLayerAPI::TileLayer_getsetters, .tp_init = (initproc)PyGridLayerAPI::TileLayer_init, diff --git a/src/McRFPy_API.cpp b/src/McRFPy_API.cpp index 8992bdf..9550b36 100644 --- a/src/McRFPy_API.cpp +++ b/src/McRFPy_API.cpp @@ -306,9 +306,7 @@ PyObject* PyInit_mcrfpy() Py_SET_TYPE(m, &McRFPyModuleType); using namespace mcrfpydef; - - // Types that are exported to Python (visible in module namespace) - PyTypeObject* exported_types[] = { + PyTypeObject* pytypes[] = { /*SFML exposed types*/ &PyColorType, /*&PyLinkedColorType,*/ &PyFontType, &PyTextureType, &PyVectorType, @@ -319,16 +317,23 @@ PyObject* PyInit_mcrfpy() &PyUICaptionType, &PyUISpriteType, &PyUIFrameType, &PyUIEntityType, &PyUIGridType, &PyUILineType, &PyUICircleType, &PyUIArcType, + /*game map & perspective data*/ + &PyUIGridPointType, &PyUIGridPointStateType, + /*grid layers (#147)*/ &PyColorLayerType, &PyTileLayerType, + /*collections & iterators*/ + &PyUICollectionType, &PyUICollectionIterType, + &PyUIEntityCollectionType, &PyUIEntityCollectionIterType, + /*animation*/ &PyAnimationType, /*timer*/ &PyTimerType, - /*window singleton type (#184 - type exported for isinstance checks)*/ + /*window singleton*/ &PyWindowType, /*scene class*/ @@ -342,18 +347,6 @@ PyObject* PyInit_mcrfpy() &PyKeyboardType, nullptr}; - - // Types that are used internally but NOT exported to module namespace (#189) - // These still need PyType_Ready() but are not added to module - PyTypeObject* internal_types[] = { - /*game map & perspective data - returned by Grid.at() but not directly instantiable*/ - &PyUIGridPointType, &PyUIGridPointStateType, - - /*collections & iterators - returned by .children/.entities but not directly instantiable*/ - &PyUICollectionType, &PyUICollectionIterType, - &PyUIEntityCollectionType, &PyUIEntityCollectionIterType, - - nullptr}; // Set up PyWindowType methods and getsetters before PyType_Ready PyWindowType.tp_methods = PyWindow::methods; @@ -374,32 +367,19 @@ PyObject* PyInit_mcrfpy() PyUICircleType.tp_weaklistoffset = offsetof(PyUICircleObject, weakreflist); PyUIArcType.tp_weaklistoffset = offsetof(PyUIArcObject, weakreflist); - // Process exported types - PyType_Ready AND add to module int i = 0; - auto t = exported_types[i]; + auto t = pytypes[i]; while (t != nullptr) { + //std::cout << "Registering type: " << t->tp_name << std::endl; if (PyType_Ready(t) < 0) { std::cout << "ERROR: PyType_Ready failed for " << t->tp_name << std::endl; return NULL; } + //std::cout << " tp_alloc after PyType_Ready: " << (void*)t->tp_alloc << std::endl; PyModule_AddType(m, t); i++; - t = exported_types[i]; - } - - // Process internal types - PyType_Ready only, NOT added to module (#189) - i = 0; - t = internal_types[i]; - while (t != nullptr) - { - if (PyType_Ready(t) < 0) { - std::cout << "ERROR: PyType_Ready failed for " << t->tp_name << std::endl; - return NULL; - } - // Note: NOT calling PyModule_AddType - these are internal-only types - i++; - t = internal_types[i]; + t = pytypes[i]; } // Add default_font and default_texture to module @@ -415,13 +395,6 @@ PyObject* PyInit_mcrfpy() PyModule_AddObject(m, "keyboard", keyboard_instance); } - // Add window singleton (#184) - // Use tp_alloc directly to bypass tp_new which blocks user instantiation - PyObject* window_instance = PyWindowType.tp_alloc(&PyWindowType, 0); - if (window_instance) { - PyModule_AddObject(m, "window", window_instance); - } - // Add version string (#164) PyModule_AddStringConstant(m, "__version__", MCRFPY_VERSION); diff --git a/src/PyDrawable.cpp b/src/PyDrawable.cpp index 61a8151..9ccadd6 100644 --- a/src/PyDrawable.cpp +++ b/src/PyDrawable.cpp @@ -123,6 +123,13 @@ static PyGetSetDef PyDrawable_getsetters[] = { {NULL} // Sentinel }; +// get_bounds method implementation (#89) +static PyObject* PyDrawable_get_bounds(PyDrawableObject* self, PyObject* Py_UNUSED(args)) +{ + auto bounds = self->data->get_bounds(); + return Py_BuildValue("(ffff)", bounds.left, bounds.top, bounds.width, bounds.height); +} + // move method implementation (#98) static PyObject* PyDrawable_move(PyDrawableObject* self, PyObject* args, PyObject* kwds) { @@ -149,6 +156,13 @@ static PyObject* PyDrawable_resize(PyDrawableObject* self, PyObject* args, PyObj // Method definitions static PyMethodDef PyDrawable_methods[] = { + {"get_bounds", (PyCFunction)PyDrawable_get_bounds, METH_NOARGS, + MCRF_METHOD(Drawable, get_bounds, + MCRF_SIG("()", "tuple"), + MCRF_DESC("Get the bounding rectangle of this drawable element."), + MCRF_RETURNS("tuple: (x, y, width, height) representing the element's bounds") + MCRF_NOTE("The bounds are in screen coordinates and account for current position and size.") + )}, {"move", (PyCFunction)PyDrawable_move, METH_VARARGS | METH_KEYWORDS, MCRF_METHOD(Drawable, move, MCRF_SIG("(dx, dy) or (delta)", "None"), diff --git a/src/UIBase.h b/src/UIBase.h index 2a89e15..36f1ab7 100644 --- a/src/UIBase.h +++ b/src/UIBase.h @@ -43,6 +43,14 @@ typedef struct { // Common Python method implementations for UIDrawable-derived classes // These template functions provide shared functionality for Python bindings +// get_bounds method implementation (#89) +template +static PyObject* UIDrawable_get_bounds(T* self, PyObject* Py_UNUSED(args)) +{ + auto bounds = self->data->get_bounds(); + return Py_BuildValue("(ffff)", bounds.left, bounds.top, bounds.width, bounds.height); +} + // move method implementation (#98) template static PyObject* UIDrawable_move(T* self, PyObject* args, PyObject* kwds) @@ -82,8 +90,14 @@ static PyObject* UIDrawable_animate(T* self, PyObject* args, PyObject* kwds) } // Macro to add common UIDrawable methods to a method array (without animate - for base types) -// #185: Removed get_bounds method - use .bounds property instead #define UIDRAWABLE_METHODS_BASE \ + {"get_bounds", (PyCFunction)UIDrawable_get_bounds, METH_NOARGS, \ + MCRF_METHOD(Drawable, get_bounds, \ + MCRF_SIG("()", "tuple"), \ + MCRF_DESC("Get the bounding rectangle of this drawable element."), \ + MCRF_RETURNS("tuple: (x, y, width, height) representing the element's bounds") \ + MCRF_NOTE("The bounds are in screen coordinates and account for current position and size.") \ + )}, \ {"move", (PyCFunction)UIDrawable_move, METH_VARARGS | METH_KEYWORDS, \ MCRF_METHOD(Drawable, move, \ MCRF_SIG("(dx, dy) or (delta)", "None"), \ @@ -202,11 +216,11 @@ static int UIDrawable_set_opacity(T* self, PyObject* value, void* closure) ), (void*)type_enum}, \ {"bounds", (getter)UIDrawable::get_bounds_py, NULL, \ MCRF_PROPERTY(bounds, \ - "Bounding box as (pos, size) tuple of Vectors. Returns (Vector(x, y), Vector(width, height))." \ + "Bounding rectangle (x, y, width, height) in local coordinates." \ ), (void*)type_enum}, \ {"global_bounds", (getter)UIDrawable::get_global_bounds_py, NULL, \ MCRF_PROPERTY(global_bounds, \ - "Bounding box as (pos, size) tuple of Vectors in screen coordinates. Returns (Vector(x, y), Vector(width, height))." \ + "Bounding rectangle (x, y, width, height) in screen coordinates." \ ), (void*)type_enum}, \ {"on_enter", (getter)UIDrawable::get_on_enter, (setter)UIDrawable::set_on_enter, \ MCRF_PROPERTY(on_enter, \ diff --git a/src/UICaption.cpp b/src/UICaption.cpp index bc10bc0..d5fb758 100644 --- a/src/UICaption.cpp +++ b/src/UICaption.cpp @@ -261,31 +261,12 @@ int UICaption::set_text(PyUICaptionObject* self, PyObject* value, void* closure) return 0; } -PyObject* UICaption::get_size(PyUICaptionObject* self, void* closure) -{ - auto bounds = self->data->text.getGlobalBounds(); - return PyVector(sf::Vector2f(bounds.width, bounds.height)).pyObject(); -} - -PyObject* UICaption::get_w(PyUICaptionObject* self, void* closure) -{ - auto bounds = self->data->text.getGlobalBounds(); - return PyFloat_FromDouble(bounds.width); -} - -PyObject* UICaption::get_h(PyUICaptionObject* self, void* closure) -{ - auto bounds = self->data->text.getGlobalBounds(); - return PyFloat_FromDouble(bounds.height); -} - PyGetSetDef UICaption::getsetters[] = { {"x", (getter)UIDrawable::get_float_member, (setter)UIDrawable::set_float_member, "X coordinate of top-left corner", (void*)((intptr_t)PyObjectsEnum::UICAPTION << 8 | 0)}, {"y", (getter)UIDrawable::get_float_member, (setter)UIDrawable::set_float_member, "Y coordinate of top-left corner", (void*)((intptr_t)PyObjectsEnum::UICAPTION << 8 | 1)}, {"pos", (getter)UIDrawable::get_pos, (setter)UIDrawable::set_pos, "(x, y) vector", (void*)PyObjectsEnum::UICAPTION}, - {"size", (getter)UICaption::get_size, NULL, "Text dimensions as Vector (read-only)", NULL}, - {"w", (getter)UICaption::get_w, NULL, "Text width in pixels (read-only)", NULL}, - {"h", (getter)UICaption::get_h, NULL, "Text height in pixels (read-only)", NULL}, + //{"w", (getter)PyUIFrame_get_float_member, (setter)PyUIFrame_set_float_member, "width of the rectangle", (void*)2}, + //{"h", (getter)PyUIFrame_get_float_member, (setter)PyUIFrame_set_float_member, "height of the rectangle", (void*)3}, {"outline", (getter)UICaption::get_float_member, (setter)UICaption::set_float_member, "Thickness of the border", (void*)4}, {"fill_color", (getter)UICaption::get_color_member, (setter)UICaption::set_color_member, "Fill color of the text. Returns a copy; modifying components requires reassignment. " diff --git a/src/UICaption.h b/src/UICaption.h index 09aa16c..a760ab0 100644 --- a/src/UICaption.h +++ b/src/UICaption.h @@ -38,9 +38,6 @@ public: static int set_color_member(PyUICaptionObject* self, PyObject* value, void* closure); static PyObject* get_text(PyUICaptionObject* self, void* closure); static int set_text(PyUICaptionObject* self, PyObject* value, void* closure); - static PyObject* get_size(PyUICaptionObject* self, void* closure); - static PyObject* get_w(PyUICaptionObject* self, void* closure); - static PyObject* get_h(PyUICaptionObject* self, void* closure); static PyGetSetDef getsetters[]; static PyObject* repr(PyUICaptionObject* self); static int init(PyUICaptionObject* self, PyObject* args, PyObject* kwds); diff --git a/src/UICollection.cpp b/src/UICollection.cpp index 73b3dde..78eedc5 100644 --- a/src/UICollection.cpp +++ b/src/UICollection.cpp @@ -1285,13 +1285,18 @@ int UICollection::init(PyUICollectionObject* self, PyObject* args, PyObject* kwd PyObject* UICollection::iter(PyUICollectionObject* self) { - // Use the iterator type directly from namespace (#189 - type not exported to module) - PyTypeObject* iterType = &PyUICollectionIterType; - + // Get the iterator type from the module to ensure we have the registered version + PyTypeObject* iterType = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "UICollectionIter"); + if (!iterType) { + PyErr_SetString(PyExc_RuntimeError, "Could not find UICollectionIter type in module"); + return NULL; + } + // Allocate new iterator instance PyUICollectionIterObject* iterObj = (PyUICollectionIterObject*)iterType->tp_alloc(iterType, 0); - + if (iterObj == NULL) { + Py_DECREF(iterType); return NULL; // Failed to allocate memory for the iterator object } @@ -1299,5 +1304,6 @@ PyObject* UICollection::iter(PyUICollectionObject* self) iterObj->index = 0; iterObj->start_size = self->data->size(); + Py_DECREF(iterType); return (PyObject*)iterObj; } diff --git a/src/UICollection.h b/src/UICollection.h index a512e71..a026ea9 100644 --- a/src/UICollection.h +++ b/src/UICollection.h @@ -42,8 +42,7 @@ public: }; namespace mcrfpydef { - // #189 - Use inline instead of static to ensure single instance across translation units - inline PyTypeObject PyUICollectionIterType = { + static PyTypeObject PyUICollectionIterType = { .ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0}, .tp_name = "mcrfpy.UICollectionIter", .tp_basicsize = sizeof(PyUICollectionIterObject), @@ -71,8 +70,7 @@ namespace mcrfpydef { } }; - // #189 - Use inline instead of static to ensure single instance across translation units - inline PyTypeObject PyUICollectionType = { + static PyTypeObject PyUICollectionType = { .ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0}, .tp_name = "mcrfpy.UICollection", .tp_basicsize = sizeof(PyUICollectionObject), diff --git a/src/UIDrawable.cpp b/src/UIDrawable.cpp index 6a32e61..7994e90 100644 --- a/src/UIDrawable.cpp +++ b/src/UIDrawable.cpp @@ -1089,7 +1089,7 @@ PyObject* UIDrawable::get_global_pos(PyObject* self, void* closure) { return result; } -// #138, #188 - Python API for bounds property - returns (pos, size) as pair of Vectors +// #138 - Python API for bounds property PyObject* UIDrawable::get_bounds_py(PyObject* self, void* closure) { PyObjectsEnum objtype = static_cast(reinterpret_cast(closure)); UIDrawable* drawable = nullptr; @@ -1122,35 +1122,10 @@ PyObject* UIDrawable::get_bounds_py(PyObject* self, void* closure) { } sf::FloatRect bounds = drawable->get_bounds(); - - // Get Vector type from mcrfpy module - PyObject* vector_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector"); - if (!vector_type) return NULL; - - // Create pos vector - PyObject* pos_args = Py_BuildValue("(ff)", bounds.left, bounds.top); - PyObject* pos = PyObject_CallObject(vector_type, pos_args); - Py_DECREF(pos_args); - if (!pos) { - Py_DECREF(vector_type); - return NULL; - } - - // Create size vector - PyObject* size_args = Py_BuildValue("(ff)", bounds.width, bounds.height); - PyObject* size = PyObject_CallObject(vector_type, size_args); - Py_DECREF(size_args); - Py_DECREF(vector_type); - if (!size) { - Py_DECREF(pos); - return NULL; - } - - // Return tuple of two vectors (N steals reference) - return Py_BuildValue("(NN)", pos, size); + return Py_BuildValue("(ffff)", bounds.left, bounds.top, bounds.width, bounds.height); } -// #138, #188 - Python API for global_bounds property - returns (pos, size) as pair of Vectors +// #138 - Python API for global_bounds property PyObject* UIDrawable::get_global_bounds_py(PyObject* self, void* closure) { PyObjectsEnum objtype = static_cast(reinterpret_cast(closure)); UIDrawable* drawable = nullptr; @@ -1183,32 +1158,7 @@ PyObject* UIDrawable::get_global_bounds_py(PyObject* self, void* closure) { } sf::FloatRect bounds = drawable->get_global_bounds(); - - // Get Vector type from mcrfpy module - PyObject* vector_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector"); - if (!vector_type) return NULL; - - // Create pos vector - PyObject* pos_args = Py_BuildValue("(ff)", bounds.left, bounds.top); - PyObject* pos = PyObject_CallObject(vector_type, pos_args); - Py_DECREF(pos_args); - if (!pos) { - Py_DECREF(vector_type); - return NULL; - } - - // Create size vector - PyObject* size_args = Py_BuildValue("(ff)", bounds.width, bounds.height); - PyObject* size = PyObject_CallObject(vector_type, size_args); - Py_DECREF(size_args); - Py_DECREF(vector_type); - if (!size) { - Py_DECREF(pos); - return NULL; - } - - // Return tuple of two vectors (N steals reference) - return Py_BuildValue("(NN)", pos, size); + return Py_BuildValue("(ffff)", bounds.left, bounds.top, bounds.width, bounds.height); } // #140 - Python API for on_enter property diff --git a/src/UIEntity.cpp b/src/UIEntity.cpp index e689670..fe13678 100644 --- a/src/UIEntity.cpp +++ b/src/UIEntity.cpp @@ -122,9 +122,9 @@ PyObject* UIEntity::at(PyUIEntityObject* self, PyObject* args, PyObject* kwds) { return NULL; } - // Use type directly since GridPointState is internal-only (not exported to module) - auto type = &mcrfpydef::PyUIGridPointStateType; + auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "GridPointState"); auto obj = (PyUIGridPointStateObject*)type->tp_alloc(type, 0); + Py_DECREF(type); obj->data = &(self->data->gridstate[y * self->data->grid->grid_x + x]); obj->grid = self->data->grid; obj->entity = self->data; @@ -320,10 +320,14 @@ sf::Vector2i PyObject_to_sfVector2i(PyObject* obj) { PyObject* UIGridPointState_to_PyObject(const UIGridPointState& state) { // Create a new GridPointState Python object (detached - no grid/entity context) - // Use type directly since GridPointState is internal-only (not exported to module) - auto type = &mcrfpydef::PyUIGridPointStateType; + auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "GridPointState"); + if (!type) { + return NULL; + } + auto obj = (PyUIGridPointStateObject*)type->tp_alloc(type, 0); if (!obj) { + Py_DECREF(type); return NULL; } @@ -338,6 +342,7 @@ PyObject* UIGridPointState_to_PyObject(const UIGridPointState& state) { obj->x = -1; obj->y = -1; + Py_DECREF(type); return (PyObject*)obj; } diff --git a/src/UIGrid.cpp b/src/UIGrid.cpp index 2910005..02c4b2a 100644 --- a/src/UIGrid.cpp +++ b/src/UIGrid.cpp @@ -6,7 +6,6 @@ #include "Profiler.h" #include "PyFOV.h" #include "PyPositionHelper.h" // For standardized position argument parsing -#include "PyVector.h" // #179, #181 - For Vector return types #include #include // #142 - for std::floor, std::isnan #include // #150 - for strcmp @@ -991,10 +990,8 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) { return 0; // Success } -// #179 - Return grid_size as Vector PyObject* UIGrid::get_grid_size(PyUIGridObject* self, void* closure) { - return PyVector(sf::Vector2f(static_cast(self->data->grid_x), - static_cast(self->data->grid_y))).pyObject(); + return Py_BuildValue("(ii)", self->data->grid_x, self->data->grid_y); } PyObject* UIGrid::get_grid_x(PyUIGridObject* self, void* closure) { @@ -1048,9 +1045,8 @@ int UIGrid::set_size(PyUIGridObject* self, PyObject* value, void* closure) { return 0; } -// #181 - Return center as Vector PyObject* UIGrid::get_center(PyUIGridObject* self, void* closure) { - return PyVector(sf::Vector2f(self->data->center_x, self->data->center_y)).pyObject(); + return Py_BuildValue("(ff)", self->data->center_x, self->data->center_y); } int UIGrid::set_center(PyUIGridObject* self, PyObject* value, void* closure) { @@ -1182,8 +1178,8 @@ PyObject* UIGrid::py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds) return NULL; } - // Use the type directly since GridPoint is internal-only (not exported to module) - auto type = &mcrfpydef::PyUIGridPointType; + //PyUIGridPointObject* obj = (PyUIGridPointObject*)((&PyUIGridPointType)->tp_alloc(&PyUIGridPointType, 0)); + auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "GridPoint"); auto obj = (PyUIGridPointObject*)type->tp_alloc(type, 0); //auto target = std::static_pointer_cast(target); // #123 - Use at() method to route through chunks for large grids @@ -3277,11 +3273,20 @@ int UIEntityCollection::init(PyUIEntityCollectionObject* self, PyObject* args, P PyObject* UIEntityCollection::iter(PyUIEntityCollectionObject* self) { - // Use the iterator type directly from namespace (#189 - type not exported to module) - PyTypeObject* iterType = &mcrfpydef::PyUIEntityCollectionIterType; + // Cache the iterator type to avoid repeated dictionary lookups (#159) + static PyTypeObject* cached_iter_type = nullptr; + if (!cached_iter_type) { + cached_iter_type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "UIEntityCollectionIter"); + if (!cached_iter_type) { + PyErr_SetString(PyExc_RuntimeError, "Could not find UIEntityCollectionIter type in module"); + return NULL; + } + // Keep a reference to prevent it from being garbage collected + Py_INCREF(cached_iter_type); + } // Allocate new iterator instance - PyUIEntityCollectionIterObject* iterObj = (PyUIEntityCollectionIterObject*)iterType->tp_alloc(iterType, 0); + PyUIEntityCollectionIterObject* iterObj = (PyUIEntityCollectionIterObject*)cached_iter_type->tp_alloc(cached_iter_type, 0); if (iterObj == NULL) { return NULL; // Failed to allocate memory for the iterator object diff --git a/src/UIGrid.h b/src/UIGrid.h index eedd1c2..ef29e6d 100644 --- a/src/UIGrid.h +++ b/src/UIGrid.h @@ -330,8 +330,7 @@ namespace mcrfpydef { } }; - // #189 - Use inline instead of static to ensure single instance across translation units - inline PyTypeObject PyUIEntityCollectionIterType = { + static PyTypeObject PyUIEntityCollectionIterType = { .ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0}, .tp_name = "mcrfpy.UIEntityCollectionIter", .tp_basicsize = sizeof(PyUIEntityCollectionIterObject), @@ -357,8 +356,7 @@ namespace mcrfpydef { } }; - // #189 - Use inline instead of static to ensure single instance across translation units - inline PyTypeObject PyUIEntityCollectionType = { + static PyTypeObject PyUIEntityCollectionType = { .ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0}, .tp_name = "mcrfpy.EntityCollection", .tp_basicsize = sizeof(PyUIEntityCollectionObject), diff --git a/src/UIGridPoint.cpp b/src/UIGridPoint.cpp index f66f0f6..757e5c1 100644 --- a/src/UIGridPoint.cpp +++ b/src/UIGridPoint.cpp @@ -2,7 +2,6 @@ #include "UIGrid.h" #include "UIEntity.h" // #114 - for GridPoint.entities #include "GridLayers.h" // #150 - for GridLayerType, ColorLayer, TileLayer -#include "McRFPy_Doc.h" // #177 - for MCRF_PROPERTY macro #include // #150 - for strcmp UIGridPoint::UIGridPoint() @@ -23,19 +22,19 @@ sf::Color PyObject_to_sfColor(PyObject* obj) { PyErr_SetString(PyExc_RuntimeError, "Failed to import mcrfpy module"); return sf::Color(); } - + PyObject* color_type = PyObject_GetAttrString(module, "Color"); Py_DECREF(module); - + if (!color_type) { PyErr_SetString(PyExc_RuntimeError, "Failed to get Color type from mcrfpy module"); return sf::Color(); } - + // Check if it's a mcrfpy.Color object int is_color = PyObject_IsInstance(obj, color_type); Py_DECREF(color_type); - + if (is_color == 1) { PyColorObject* color_obj = (PyColorObject*)obj; return color_obj->data; @@ -43,7 +42,7 @@ sf::Color PyObject_to_sfColor(PyObject* obj) { // Error occurred in PyObject_IsInstance return sf::Color(); } - + // Otherwise try to parse as tuple int r, g, b, a = 255; // Default alpha to fully opaque if not specified if (!PyArg_ParseTuple(obj, "iii|i", &r, &g, &b, &a)) { @@ -81,12 +80,12 @@ int UIGridPoint::set_bool_member(PyUIGridPointObject* self, PyObject* value, voi PyErr_SetString(PyExc_ValueError, "Expected a boolean value"); return -1; } - + // Sync with TCOD map if parent grid exists if (self->data->parent_grid && self->data->grid_x >= 0 && self->data->grid_y >= 0) { self->data->parent_grid->syncTCODMapCell(self->data->grid_x, self->data->grid_y); } - + return 0; } @@ -134,17 +133,10 @@ PyObject* UIGridPoint::get_entities(PyUIGridPointObject* self, void* closure) { return list; } -// #177 - Get grid position as tuple -PyObject* UIGridPoint::get_grid_pos(PyUIGridPointObject* self, void* closure) { - return Py_BuildValue("(ii)", self->data->grid_x, self->data->grid_y); -} - PyGetSetDef UIGridPoint::getsetters[] = { {"walkable", (getter)UIGridPoint::get_bool_member, (setter)UIGridPoint::set_bool_member, "Is the GridPoint walkable", (void*)0}, {"transparent", (getter)UIGridPoint::get_bool_member, (setter)UIGridPoint::set_bool_member, "Is the GridPoint transparent", (void*)1}, {"entities", (getter)UIGridPoint::get_entities, NULL, "List of entities at this grid cell (read-only)", NULL}, - {"grid_pos", (getter)UIGridPoint::get_grid_pos, NULL, - MCRF_PROPERTY(grid_pos, "Grid coordinates as (x, y) tuple (read-only)."), NULL}, {NULL} /* Sentinel */ }; @@ -201,9 +193,12 @@ PyObject* UIGridPointState::get_point(PyUIGridPointStateObject* self, void* clos return NULL; } - // Return the GridPoint at this position (use type directly since it's internal-only) - auto type = &mcrfpydef::PyUIGridPointType; + // Return the GridPoint at this position + auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "GridPoint"); + if (!type) return NULL; + auto obj = (PyUIGridPointObject*)type->tp_alloc(type, 0); + Py_DECREF(type); if (!obj) return NULL; // Get the GridPoint from the grid diff --git a/src/UIGridPoint.h b/src/UIGridPoint.h index c62b14f..16b01a4 100644 --- a/src/UIGridPoint.h +++ b/src/UIGridPoint.h @@ -53,9 +53,6 @@ public: // #114 - entities property: list of entities at this cell static PyObject* get_entities(PyUIGridPointObject* self, void* closure); - // #177 - grid_pos property: grid coordinates as tuple - static PyObject* get_grid_pos(PyUIGridPointObject* self, void* closure); - // #150 - Dynamic property access for named layers static PyObject* getattro(PyUIGridPointObject* self, PyObject* name); static int setattro(PyUIGridPointObject* self, PyObject* name, PyObject* value); @@ -77,8 +74,7 @@ public: }; namespace mcrfpydef { - // #189 - Use inline instead of static to ensure single instance across translation units - inline PyTypeObject PyUIGridPointType = { + static PyTypeObject PyUIGridPointType = { .ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0}, .tp_name = "mcrfpy.GridPoint", .tp_basicsize = sizeof(PyUIGridPointObject), @@ -94,8 +90,7 @@ namespace mcrfpydef { .tp_new = NULL, // Prevent instantiation from Python - Issue #12 }; - // #189 - Use inline instead of static to ensure single instance across translation units - inline PyTypeObject PyUIGridPointStateType = { + static PyTypeObject PyUIGridPointStateType = { .ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0}, .tp_name = "mcrfpy.GridPointState", .tp_basicsize = sizeof(PyUIGridPointStateObject), diff --git a/tests/issue_190_layer_docs_test.py b/tests/issue_190_layer_docs_test.py deleted file mode 100644 index 81e4d9e..0000000 --- a/tests/issue_190_layer_docs_test.py +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/env python3 -"""Test for issue #190: Expanded TileLayer and ColorLayer __init__ documentation. - -This test verifies that the documentation for ColorLayer and TileLayer -contains the expected key phrases and is comprehensive. -""" -import mcrfpy -import sys - -def test_colorlayer_docs(): - """Test ColorLayer documentation completeness.""" - doc = mcrfpy.ColorLayer.__doc__ - - if not doc: - print("FAIL: ColorLayer.__doc__ is empty or None") - return False - - print("=== ColorLayer Documentation ===") - print(doc) - print() - - # Check for key phrases - required_phrases = [ - "grid_size", - "z_index", - "RGBA", - "at(x, y)", - "set(x, y", - "fill(", - "Grid.add_layer", - "visible", - "Example", - ] - - missing = [] - for phrase in required_phrases: - if phrase not in doc: - missing.append(phrase) - - if missing: - print(f"FAIL: ColorLayer docs missing phrases: {missing}") - return False - - print("ColorLayer documentation: PASS") - return True - -def test_tilelayer_docs(): - """Test TileLayer documentation completeness.""" - doc = mcrfpy.TileLayer.__doc__ - - if not doc: - print("FAIL: TileLayer.__doc__ is empty or None") - return False - - print("=== TileLayer Documentation ===") - print(doc) - print() - - # Check for key phrases - required_phrases = [ - "grid_size", - "z_index", - "texture", - "at(x, y)", - "set(x, y", - "fill(", - "-1", # Special value for no tile - "sprite", - "Grid.add_layer", - "visible", - "Example", - ] - - missing = [] - for phrase in required_phrases: - if phrase not in doc: - missing.append(phrase) - - if missing: - print(f"FAIL: TileLayer docs missing phrases: {missing}") - return False - - print("TileLayer documentation: PASS") - return True - -# Run tests -colorlayer_ok = test_colorlayer_docs() -tilelayer_ok = test_tilelayer_docs() - -if colorlayer_ok and tilelayer_ok: - print("\nAll documentation tests PASSED") - sys.exit(0) -else: - print("\nDocumentation tests FAILED") - sys.exit(1) diff --git a/tests/test_caption_size.py b/tests/test_caption_size.py deleted file mode 100644 index 41d314b..0000000 --- a/tests/test_caption_size.py +++ /dev/null @@ -1,87 +0,0 @@ -#!/usr/bin/env python3 -"""Test Caption size/w/h properties""" -import sys -import mcrfpy - -print("Testing Caption size/w/h properties...") - -font = mcrfpy.Font("assets/JetbrainsMono.ttf") -caption = mcrfpy.Caption(text="Test Caption", pos=(100, 100), font=font) -print(f"Caption created: {caption}") - -# Test size property -print("Testing size property...") -size = caption.size -print(f"size type: {type(size)}") -print(f"size value: {size}") - -if not hasattr(size, 'x'): - print(f"FAIL: size should be Vector, got {type(size)}") - sys.exit(1) - -print(f"size.x={size.x}, size.y={size.y}") - -if size.x <= 0 or size.y <= 0: - print(f"FAIL: size should be positive, got ({size.x}, {size.y})") - sys.exit(1) - -# Test w property -print("Testing w property...") -w = caption.w -print(f"w type: {type(w)}, value: {w}") - -if not isinstance(w, float): - print(f"FAIL: w should be float, got {type(w)}") - sys.exit(1) - -if w <= 0: - print(f"FAIL: w should be positive, got {w}") - sys.exit(1) - -# Test h property -print("Testing h property...") -h = caption.h -print(f"h type: {type(h)}, value: {h}") - -if not isinstance(h, float): - print(f"FAIL: h should be float, got {type(h)}") - sys.exit(1) - -if h <= 0: - print(f"FAIL: h should be positive, got {h}") - sys.exit(1) - -# Verify w and h match size -if abs(w - size.x) >= 0.001: - print(f"FAIL: w ({w}) should match size.x ({size.x})") - sys.exit(1) - -if abs(h - size.y) >= 0.001: - print(f"FAIL: h ({h}) should match size.y ({size.y})") - sys.exit(1) - -# Verify read-only -print("Checking that size/w/h are read-only...") -try: - caption.size = mcrfpy.Vector(100, 100) - print("FAIL: size should be read-only") - sys.exit(1) -except AttributeError: - print(" size is correctly read-only") - -try: - caption.w = 100 - print("FAIL: w should be read-only") - sys.exit(1) -except AttributeError: - print(" w is correctly read-only") - -try: - caption.h = 100 - print("FAIL: h should be read-only") - sys.exit(1) -except AttributeError: - print(" h is correctly read-only") - -print("PASS: Caption size/w/h properties work correctly!") -sys.exit(0) diff --git a/tests/test_frame_bounds.py b/tests/test_frame_bounds.py deleted file mode 100644 index c74f900..0000000 --- a/tests/test_frame_bounds.py +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env python3 -"""Test Frame bounds""" -import sys -import mcrfpy - -print("Testing Frame bounds...") -frame = mcrfpy.Frame(pos=(50, 100), size=(200, 150)) - -print(f"Frame created: {frame}") - -# Test bounds returns tuple of Vectors -bounds = frame.bounds -print(f"bounds type: {type(bounds)}") -print(f"bounds value: {bounds}") - -if not isinstance(bounds, tuple): - print(f"FAIL: bounds should be tuple, got {type(bounds)}") - sys.exit(1) - -if len(bounds) != 2: - print(f"FAIL: bounds should have 2 elements, got {len(bounds)}") - sys.exit(1) - -pos, size = bounds -print(f"pos type: {type(pos)}, value: {pos}") -print(f"size type: {type(size)}, value: {size}") - -if not hasattr(pos, 'x'): - print(f"FAIL: pos should be Vector (has no .x), got {type(pos)}") - sys.exit(1) - -print(f"pos.x={pos.x}, pos.y={pos.y}") -print(f"size.x={size.x}, size.y={size.y}") - -# Test get_bounds() method is removed (#185) -if hasattr(frame, 'get_bounds'): - print("FAIL: get_bounds() method should be removed") - sys.exit(1) -else: - print("PASS: get_bounds() method is removed") - -print("PASS: Frame bounds test passed!") -sys.exit(0) diff --git a/tests/test_grid_features.py b/tests/test_grid_features.py deleted file mode 100644 index 7e407e1..0000000 --- a/tests/test_grid_features.py +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env python3 -"""Test Grid features""" -import sys -import mcrfpy - -print("Testing Grid features...") - -# Create a texture first -print("Loading texture...") -texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16) -print(f"Texture loaded: {texture}") - -# Create grid -print("Creating grid...") -grid = mcrfpy.Grid(grid_size=(15, 20), texture=texture, pos=(50, 100), size=(240, 320)) -print(f"Grid created: {grid}") - -# Test grid_size returns Vector -print("Testing grid_size...") -grid_size = grid.grid_size -print(f"grid_size type: {type(grid_size)}") -print(f"grid_size value: {grid_size}") - -if not hasattr(grid_size, 'x'): - print(f"FAIL: grid_size should be Vector, got {type(grid_size)}") - sys.exit(1) - -print(f"grid_size.x={grid_size.x}, grid_size.y={grid_size.y}") - -if grid_size.x != 15 or grid_size.y != 20: - print(f"FAIL: grid_size should be (15, 20), got ({grid_size.x}, {grid_size.y})") - sys.exit(1) - -# Test center returns Vector -print("Testing center...") -center = grid.center -print(f"center type: {type(center)}") -print(f"center value: {center}") - -if not hasattr(center, 'x'): - print(f"FAIL: center should be Vector, got {type(center)}") - sys.exit(1) - -print(f"center.x={center.x}, center.y={center.y}") - -# Test pos returns Vector -print("Testing pos...") -pos = grid.pos -print(f"pos type: {type(pos)}") - -if not hasattr(pos, 'x'): - print(f"FAIL: pos should be Vector, got {type(pos)}") - sys.exit(1) - -print(f"pos.x={pos.x}, pos.y={pos.y}") - -print("PASS: Grid Vector properties work correctly!") -sys.exit(0) diff --git a/tests/test_layer_docs.py b/tests/test_layer_docs.py deleted file mode 100644 index 033cb7e..0000000 --- a/tests/test_layer_docs.py +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env python3 -"""Test layer documentation""" -import sys -import mcrfpy - -print("Testing layer documentation (#190)...") - -# Verify layer types exist and have docstrings -print("Checking TileLayer...") -if not hasattr(mcrfpy, 'TileLayer'): - print("FAIL: TileLayer should exist") - sys.exit(1) - -print("Checking ColorLayer...") -if not hasattr(mcrfpy, 'ColorLayer'): - print("FAIL: ColorLayer should exist") - sys.exit(1) - -# Check that docstrings exist and contain useful info -tile_doc = mcrfpy.TileLayer.__doc__ -color_doc = mcrfpy.ColorLayer.__doc__ - -print(f"TileLayer.__doc__ length: {len(tile_doc) if tile_doc else 0}") -print(f"ColorLayer.__doc__ length: {len(color_doc) if color_doc else 0}") - -if tile_doc is None or len(tile_doc) < 50: - print(f"FAIL: TileLayer should have substantial docstring") - sys.exit(1) - -if color_doc is None or len(color_doc) < 50: - print(f"FAIL: ColorLayer should have substantial docstring") - sys.exit(1) - -print("PASS: Layer documentation exists!") -sys.exit(0) diff --git a/tests/test_module_namespace.py b/tests/test_module_namespace.py deleted file mode 100644 index 6f25dc4..0000000 --- a/tests/test_module_namespace.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env python3 -"""Test module namespace changes (#184, #189)""" -import sys -import mcrfpy - -print("Testing module namespace changes (#184, #189)...") - -# Test window singleton exists (#184) -print("Testing window singleton...") -if not hasattr(mcrfpy, 'window'): - print("FAIL: mcrfpy.window should exist") - sys.exit(1) - -window = mcrfpy.window -if window is None: - print("FAIL: window should not be None") - sys.exit(1) - -# Verify window properties -if not hasattr(window, 'resolution'): - print("FAIL: window should have resolution property") - sys.exit(1) - -print(f" window exists: {window}") -print(f" window.resolution: {window.resolution}") - -# Test that internal types are hidden from module namespace (#189) -print("Testing hidden internal types...") -hidden_types = ['UICollectionIter', 'UIEntityCollectionIter', 'GridPoint', 'GridPointState'] -visible = [] -for name in hidden_types: - if hasattr(mcrfpy, name): - visible.append(name) - -if visible: - print(f"FAIL: These types should be hidden from module namespace: {visible}") - # Note: This is a soft fail - if these are expected to be visible, adjust the test - # sys.exit(1) -else: - print(" All internal types are hidden from module namespace") - -# But iteration should still work - test UICollection iteration -print("Testing that iteration still works...") -scene = mcrfpy.Scene("test_scene") -ui = scene.children -ui.append(mcrfpy.Frame(pos=(0,0), size=(50,50))) -ui.append(mcrfpy.Caption(text="hi", pos=(0,0))) - -count = 0 -for item in ui: - count += 1 - print(f" Iterated item: {item}") - -if count != 2: - print(f"FAIL: Should iterate over 2 items, got {count}") - sys.exit(1) - -print(" Iteration works correctly") - -print("PASS: Module namespace changes work correctly!") -sys.exit(0)