diff --git a/src/GameEngine.cpp b/src/GameEngine.cpp index 351214d..fea99df 100644 --- a/src/GameEngine.cpp +++ b/src/GameEngine.cpp @@ -73,11 +73,32 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg) GameEngine::~GameEngine() { + cleanup(); for (auto& [name, scene] : scenes) { delete scene; } } +void GameEngine::cleanup() +{ + if (cleaned_up) return; + cleaned_up = true; + + // Clear Python references before destroying C++ objects + // Clear all timers (they hold Python callables) + timers.clear(); + + // Clear McRFPy_API's reference to this game engine + if (McRFPy_API::game == this) { + McRFPy_API::game = nullptr; + } + + // Force close the window if it's still open + if (window && window->isOpen()) { + window->close(); + } +} + Scene* GameEngine::currentScene() { return scenes[scene]; } void GameEngine::changeScene(std::string s) { @@ -162,6 +183,9 @@ void GameEngine::run() running = false; } } + + // Clean up before exiting the run loop + cleanup(); } std::shared_ptr GameEngine::getTimer(const std::string& name) diff --git a/src/GameEngine.h b/src/GameEngine.h index f491a09..8b7a198 100644 --- a/src/GameEngine.h +++ b/src/GameEngine.h @@ -28,6 +28,7 @@ class GameEngine bool headless = false; McRogueFaceConfig config; + bool cleaned_up = false; void testTimers(); @@ -50,6 +51,7 @@ public: sf::RenderTarget* getRenderTargetPtr() { return render_target; } void run(); void sUserInput(); + void cleanup(); // Clean up Python references before destruction int getFrame() { return currentFrame; } float getFrameTime() { return frameTime; } sf::View getView() { return visible; } diff --git a/src/UIBase.h b/src/UIBase.h index 70a5872..c1707bf 100644 --- a/src/UIBase.h +++ b/src/UIBase.h @@ -1,4 +1,6 @@ #pragma once +#include "Python.h" +#include class UIEntity; typedef struct { @@ -30,3 +32,103 @@ typedef struct { PyObject_HEAD std::shared_ptr data; } PyUISpriteObject; + +// 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) +{ + float dx, dy; + if (!PyArg_ParseTuple(args, "ff", &dx, &dy)) { + return NULL; + } + + self->data->move(dx, dy); + Py_RETURN_NONE; +} + +// resize method implementation (#98) +template +static PyObject* UIDrawable_resize(T* self, PyObject* args) +{ + float w, h; + if (!PyArg_ParseTuple(args, "ff", &w, &h)) { + return NULL; + } + + self->data->resize(w, h); + Py_RETURN_NONE; +} + +// Macro to add common UIDrawable methods to a method array +#define UIDRAWABLE_METHODS \ + {"get_bounds", (PyCFunction)UIDrawable_get_bounds, METH_NOARGS, \ + "Get bounding box as (x, y, width, height)"}, \ + {"move", (PyCFunction)UIDrawable_move, METH_VARARGS, \ + "Move by relative offset (dx, dy)"}, \ + {"resize", (PyCFunction)UIDrawable_resize, METH_VARARGS, \ + "Resize to new dimensions (width, height)"} + +// Property getters/setters for visible and opacity +template +static PyObject* UIDrawable_get_visible(T* self, void* closure) +{ + return PyBool_FromLong(self->data->visible); +} + +template +static int UIDrawable_set_visible(T* self, PyObject* value, void* closure) +{ + if (!PyBool_Check(value)) { + PyErr_SetString(PyExc_TypeError, "visible must be a boolean"); + return -1; + } + self->data->visible = PyObject_IsTrue(value); + return 0; +} + +template +static PyObject* UIDrawable_get_opacity(T* self, void* closure) +{ + return PyFloat_FromDouble(self->data->opacity); +} + +template +static int UIDrawable_set_opacity(T* self, PyObject* value, void* closure) +{ + float opacity; + if (PyFloat_Check(value)) { + opacity = PyFloat_AsDouble(value); + } else if (PyLong_Check(value)) { + opacity = PyLong_AsDouble(value); + } else { + PyErr_SetString(PyExc_TypeError, "opacity must be a number"); + return -1; + } + + // Clamp to valid range + if (opacity < 0.0f) opacity = 0.0f; + if (opacity > 1.0f) opacity = 1.0f; + + self->data->opacity = opacity; + return 0; +} + +// Macro to add common UIDrawable properties to a getsetters array +#define UIDRAWABLE_GETSETTERS \ + {"visible", (getter)UIDrawable_get_visible, (setter)UIDrawable_set_visible, \ + "Visibility flag", NULL}, \ + {"opacity", (getter)UIDrawable_get_opacity, (setter)UIDrawable_set_opacity, \ + "Opacity (0.0 = transparent, 1.0 = opaque)", NULL} + +// UIEntity specializations are defined in UIEntity.cpp after UIEntity class is complete diff --git a/src/UICaption.cpp b/src/UICaption.cpp index 2c62bda..940e0f9 100644 --- a/src/UICaption.cpp +++ b/src/UICaption.cpp @@ -4,7 +4,7 @@ #include "PyVector.h" #include "PyFont.h" #include "PyPositionHelper.h" -#include "UIDrawable_methods.h" +// UIDrawable methods now in UIBase.h #include UICaption::UICaption() @@ -277,7 +277,7 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds) { using namespace mcrfpydef; - static const char* keywords[] = { "x", "y", "text", "font", "fill_color", "outline_color", "outline", "click", "pos", nullptr }; + static const char* keywords[] = { "text", "x", "y", "font", "fill_color", "outline_color", "outline", "click", "pos", nullptr }; float x = 0.0f, y = 0.0f, outline = 0.0f; char* text = NULL; PyObject* font = NULL; @@ -286,39 +286,72 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds) PyObject* click_handler = NULL; PyObject* pos_obj = NULL; - // Try parsing all arguments with keywords - if (PyArg_ParseTupleAndKeywords(args, kwds, "|ffzOOOfOO", - const_cast(keywords), - &x, &y, &text, &font, &fill_color, &outline_color, &outline, &click_handler, &pos_obj)) - { - // If pos was provided, it overrides x,y - if (pos_obj && pos_obj != Py_None) { - PyVectorObject* vec = PyVector::from_arg(pos_obj); - if (!vec) { - PyErr_SetString(PyExc_TypeError, "pos must be a Vector or tuple (x, y)"); + // Handle different argument patterns + Py_ssize_t args_size = PyTuple_Size(args); + + if (args_size >= 2 && !PyUnicode_Check(PyTuple_GetItem(args, 0))) { + // Pattern 1: (x, y, text, ...) or ((x,y), text, ...) + PyObject* first_arg = PyTuple_GetItem(args, 0); + + // Check if first arg is a tuple/Vector (pos format) + if (PyTuple_Check(first_arg) || PyObject_IsInstance(first_arg, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector"))) { + // Pattern: ((x,y), text, ...) + static const char* pos_keywords[] = { "pos", "text", "font", "fill_color", "outline_color", "outline", "click", "x", "y", nullptr }; + PyObject* pos = NULL; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OzOOOfOff", + const_cast(pos_keywords), + &pos, &text, &font, &fill_color, &outline_color, &outline, &click_handler, &x, &y)) + { return -1; } - x = vec->data.x; - y = vec->data.y; + + // Parse position + if (pos && pos != Py_None) { + PyVectorObject* vec = PyVector::from_arg(pos); + if (!vec) { + PyErr_SetString(PyExc_TypeError, "pos must be a Vector or tuple (x, y)"); + return -1; + } + x = vec->data.x; + y = vec->data.y; + } + } + else { + // Pattern: (x, y, text, ...) + static const char* xy_keywords[] = { "x", "y", "text", "font", "fill_color", "outline_color", "outline", "click", "pos", nullptr }; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffzOOOfOO", + const_cast(xy_keywords), + &x, &y, &text, &font, &fill_color, &outline_color, &outline, &click_handler, &pos_obj)) + { + return -1; + } + + // If pos was provided, it overrides x,y + if (pos_obj && pos_obj != Py_None) { + PyVectorObject* vec = PyVector::from_arg(pos_obj); + if (!vec) { + PyErr_SetString(PyExc_TypeError, "pos must be a Vector or tuple (x, y)"); + return -1; + } + x = vec->data.x; + y = vec->data.y; + } } } else { - PyErr_Clear(); - - // Try alternative: first arg is pos tuple/Vector - static const char* alt_keywords[] = { "pos", "text", "font", "fill_color", "outline_color", "outline", "click", nullptr }; - PyObject* pos = NULL; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OzOOOfO", - const_cast(alt_keywords), - &pos, &text, &font, &fill_color, &outline_color, &outline, &click_handler)) + // Pattern 2: (text, ...) with x, y as keywords + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|zffOOOfOO", + const_cast(keywords), + &text, &x, &y, &font, &fill_color, &outline_color, &outline, &click_handler, &pos_obj)) { return -1; } - // Parse position - if (pos && pos != Py_None) { - PyVectorObject* vec = PyVector::from_arg(pos); + // If pos was provided, it overrides x,y + if (pos_obj && pos_obj != Py_None) { + PyVectorObject* vec = PyVector::from_arg(pos_obj); if (!vec) { PyErr_SetString(PyExc_TypeError, "pos must be a Vector or tuple (x, y)"); return -1; diff --git a/src/UIDrawable_methods.h b/src/UIDrawable_methods.h deleted file mode 100644 index baabe27..0000000 --- a/src/UIDrawable_methods.h +++ /dev/null @@ -1,193 +0,0 @@ -#pragma once -#include "Python.h" -#include "UIDrawable.h" -#include "UIBase.h" // For PyUIEntityObject - -// Common methods for all UIDrawable-derived classes - -// 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) -{ - float dx, dy; - if (!PyArg_ParseTuple(args, "ff", &dx, &dy)) { - return NULL; - } - - self->data->move(dx, dy); - Py_RETURN_NONE; -} - -// resize method implementation (#98) -template -static PyObject* UIDrawable_resize(T* self, PyObject* args) -{ - float w, h; - if (!PyArg_ParseTuple(args, "ff", &w, &h)) { - return NULL; - } - - self->data->resize(w, h); - Py_RETURN_NONE; -} - -// Macro to add common UIDrawable methods to a method array -#define UIDRAWABLE_METHODS \ - {"get_bounds", (PyCFunction)UIDrawable_get_bounds, METH_NOARGS, \ - "Get bounding box as (x, y, width, height)"}, \ - {"move", (PyCFunction)UIDrawable_move, METH_VARARGS, \ - "Move by relative offset (dx, dy)"}, \ - {"resize", (PyCFunction)UIDrawable_resize, METH_VARARGS, \ - "Resize to new dimensions (width, height)"} - -// Common getsetters for UIDrawable properties -#define UIDRAWABLE_GETSETTERS \ - {"visible", (getter)UIDrawable_get_visible, (setter)UIDrawable_set_visible, \ - "Whether the object is visible", NULL}, \ - {"opacity", (getter)UIDrawable_get_opacity, (setter)UIDrawable_set_opacity, \ - "Opacity level (0.0 = transparent, 1.0 = opaque)", NULL} - -// Visible property getter (new for #87) -template -static PyObject* UIDrawable_get_visible(T* self, void* closure) -{ - return PyBool_FromLong(self->data->visible); -} - -// Visible property setter (new for #87) -template -static int UIDrawable_set_visible(T* self, PyObject* value, void* closure) -{ - if (!PyBool_Check(value)) { - PyErr_SetString(PyExc_TypeError, "visible must be a boolean"); - return -1; - } - - self->data->visible = (value == Py_True); - return 0; -} - -// Opacity property getter (new for #88) -template -static PyObject* UIDrawable_get_opacity(T* self, void* closure) -{ - return PyFloat_FromDouble(self->data->opacity); -} - -// Opacity property setter (new for #88) -template -static int UIDrawable_set_opacity(T* self, PyObject* value, void* closure) -{ - float val; - if (PyFloat_Check(value)) { - val = PyFloat_AsDouble(value); - } else if (PyLong_Check(value)) { - val = PyLong_AsLong(value); - } else { - PyErr_SetString(PyExc_TypeError, "opacity must be a number"); - return -1; - } - - // Clamp to valid range - if (val < 0.0f) val = 0.0f; - if (val > 1.0f) val = 1.0f; - - self->data->opacity = val; - return 0; -} - -// Specializations for UIEntity that delegate to its sprite member -template<> -inline PyObject* UIDrawable_get_visible(PyUIEntityObject* self, void* closure) -{ - return PyBool_FromLong(self->data->sprite.visible); -} - -template<> -inline int UIDrawable_set_visible(PyUIEntityObject* self, PyObject* value, void* closure) -{ - if (!PyBool_Check(value)) { - PyErr_SetString(PyExc_TypeError, "visible must be a boolean"); - return -1; - } - - self->data->sprite.visible = (value == Py_True); - return 0; -} - -template<> -inline PyObject* UIDrawable_get_opacity(PyUIEntityObject* self, void* closure) -{ - return PyFloat_FromDouble(self->data->sprite.opacity); -} - -template<> -inline int UIDrawable_set_opacity(PyUIEntityObject* self, PyObject* value, void* closure) -{ - float val; - if (PyFloat_Check(value)) { - val = PyFloat_AsDouble(value); - } else if (PyLong_Check(value)) { - val = PyLong_AsLong(value); - } else { - PyErr_SetString(PyExc_TypeError, "opacity must be a number"); - return -1; - } - - // Clamp to valid range - if (val < 0.0f) val = 0.0f; - if (val > 1.0f) val = 1.0f; - - self->data->sprite.opacity = val; - return 0; -} - -// For get_bounds - UIEntity doesn't have this method, so we delegate to sprite -template<> -inline PyObject* UIDrawable_get_bounds(PyUIEntityObject* self, PyObject* Py_UNUSED(args)) -{ - auto bounds = self->data->sprite.get_bounds(); - return Py_BuildValue("(ffff)", bounds.left, bounds.top, bounds.width, bounds.height); -} - -// For move - UIEntity needs to update its position -template<> -inline PyObject* UIDrawable_move(PyUIEntityObject* self, PyObject* args) -{ - float dx, dy; - if (!PyArg_ParseTuple(args, "ff", &dx, &dy)) { - return NULL; - } - - // Update entity position - self->data->position.x += dx; - self->data->position.y += dy; - self->data->collision_pos.x = std::floor(self->data->position.x); - self->data->collision_pos.y = std::floor(self->data->position.y); - - // Also update sprite position - self->data->sprite.move(dx, dy); - - Py_RETURN_NONE; -} - -// For resize - delegate to sprite -template<> -inline PyObject* UIDrawable_resize(PyUIEntityObject* self, PyObject* args) -{ - float w, h; - if (!PyArg_ParseTuple(args, "ff", &w, &h)) { - return NULL; - } - - self->data->sprite.resize(w, h); - Py_RETURN_NONE; -} \ No newline at end of file diff --git a/src/UIEntity.cpp b/src/UIEntity.cpp index 13f37c9..f24af50 100644 --- a/src/UIEntity.cpp +++ b/src/UIEntity.cpp @@ -5,7 +5,8 @@ #include "PyObjectUtils.h" #include "PyVector.h" #include "PyPositionHelper.h" -#include "UIDrawable_methods.h" +// UIDrawable methods now in UIBase.h +#include "UIEntityPyMethods.h" UIEntity::UIEntity() @@ -383,7 +384,8 @@ PyGetSetDef UIEntity::getsetters[] = { {"sprite_number", (getter)UIEntity::get_spritenumber, (setter)UIEntity::set_spritenumber, "Sprite index on the texture on the display (deprecated: use sprite_index)", NULL}, {"x", (getter)UIEntity::get_float_member, (setter)UIEntity::set_float_member, "Entity x position", (void*)0}, {"y", (getter)UIEntity::get_float_member, (setter)UIEntity::set_float_member, "Entity y position", (void*)1}, - UIDRAWABLE_GETSETTERS, + {"visible", (getter)UIEntity_get_visible, (setter)UIEntity_set_visible, "Visibility flag", NULL}, + {"opacity", (getter)UIEntity_get_opacity, (setter)UIEntity_set_opacity, "Opacity (0.0 = transparent, 1.0 = opaque)", NULL}, {NULL} /* Sentinel */ }; diff --git a/src/UIEntity.h b/src/UIEntity.h index 61a0c79..0ad7d88 100644 --- a/src/UIEntity.h +++ b/src/UIEntity.h @@ -51,6 +51,11 @@ public: bool setProperty(const std::string& name, int value); bool getProperty(const std::string& name, float& value) const; + // Methods that delegate to sprite + sf::FloatRect get_bounds() const { return sprite.get_bounds(); } + void move(float dx, float dy) { sprite.move(dx, dy); position.x += dx; position.y += dy; } + void resize(float w, float h) { /* Entities don't support direct resizing */ } + static PyObject* at(PyUIEntityObject* self, PyObject* o); static PyObject* index(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored)); static PyObject* die(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored)); diff --git a/src/UIEntityPyMethods.h b/src/UIEntityPyMethods.h new file mode 100644 index 0000000..b5f8014 --- /dev/null +++ b/src/UIEntityPyMethods.h @@ -0,0 +1,48 @@ +#pragma once +#include "UIEntity.h" +#include "UIBase.h" + +// UIEntity-specific property implementations +// These delegate to the wrapped sprite member + +// Visible property +static PyObject* UIEntity_get_visible(PyUIEntityObject* self, void* closure) +{ + return PyBool_FromLong(self->data->sprite.visible); +} + +static int UIEntity_set_visible(PyUIEntityObject* self, PyObject* value, void* closure) +{ + if (!PyBool_Check(value)) { + PyErr_SetString(PyExc_TypeError, "visible must be a boolean"); + return -1; + } + self->data->sprite.visible = PyObject_IsTrue(value); + return 0; +} + +// Opacity property +static PyObject* UIEntity_get_opacity(PyUIEntityObject* self, void* closure) +{ + return PyFloat_FromDouble(self->data->sprite.opacity); +} + +static int UIEntity_set_opacity(PyUIEntityObject* self, PyObject* value, void* closure) +{ + float opacity; + if (PyFloat_Check(value)) { + opacity = PyFloat_AsDouble(value); + } else if (PyLong_Check(value)) { + opacity = PyLong_AsDouble(value); + } else { + PyErr_SetString(PyExc_TypeError, "opacity must be a number"); + return -1; + } + + // Clamp to valid range + if (opacity < 0.0f) opacity = 0.0f; + if (opacity > 1.0f) opacity = 1.0f; + + self->data->sprite.opacity = opacity; + return 0; +} \ No newline at end of file diff --git a/src/UIFrame.cpp b/src/UIFrame.cpp index f744a77..d6b2ffa 100644 --- a/src/UIFrame.cpp +++ b/src/UIFrame.cpp @@ -7,7 +7,7 @@ #include "UIGrid.h" #include "McRFPy_API.h" #include "PyPositionHelper.h" -#include "UIDrawable_methods.h" +// UIDrawable methods now in UIBase.h UIDrawable* UIFrame::click_at(sf::Vector2f point) { diff --git a/src/UIGrid.cpp b/src/UIGrid.cpp index ad599aa..8a0b6b8 100644 --- a/src/UIGrid.cpp +++ b/src/UIGrid.cpp @@ -3,7 +3,7 @@ #include "McRFPy_API.h" #include "PyPositionHelper.h" #include -#include "UIDrawable_methods.h" +// UIDrawable methods now in UIBase.h UIGrid::UIGrid() : grid_x(0), grid_y(0), zoom(1.0f), center_x(0.0f), center_y(0.0f), ptex(nullptr) diff --git a/src/UISprite.cpp b/src/UISprite.cpp index ba3c5fd..6eeb240 100644 --- a/src/UISprite.cpp +++ b/src/UISprite.cpp @@ -2,7 +2,7 @@ #include "GameEngine.h" #include "PyVector.h" #include "PyPositionHelper.h" -#include "UIDrawable_methods.h" +// UIDrawable methods now in UIBase.h UIDrawable* UISprite::click_at(sf::Vector2f point) {