fix: prevent segfault when closing window via X button

- Add cleanup() method to GameEngine to clear Python references before destruction
- Clear timers and McRFPy_API references in proper order
- Call cleanup() at end of run loop and in destructor
- Ensure cleanup is only called once per GameEngine instance

Also includes:
- Fix audio ::stop() calls (already in place, OpenAL warning is benign)
- Add Caption support for x, y keywords (e.g. Caption("text", x=5, y=10))
- Refactor UIDrawable_methods.h into UIBase.h for better organization
- Move UIEntity-specific implementations to UIEntityPyMethods.h

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
John McCardle 2025-07-06 10:08:42 -04:00
parent ee6550bf63
commit 97067a104e
11 changed files with 247 additions and 224 deletions

View File

@ -73,11 +73,32 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg)
GameEngine::~GameEngine() GameEngine::~GameEngine()
{ {
cleanup();
for (auto& [name, scene] : scenes) { for (auto& [name, scene] : scenes) {
delete scene; 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]; } Scene* GameEngine::currentScene() { return scenes[scene]; }
void GameEngine::changeScene(std::string s) void GameEngine::changeScene(std::string s)
{ {
@ -162,6 +183,9 @@ void GameEngine::run()
running = false; running = false;
} }
} }
// Clean up before exiting the run loop
cleanup();
} }
std::shared_ptr<PyTimerCallable> GameEngine::getTimer(const std::string& name) std::shared_ptr<PyTimerCallable> GameEngine::getTimer(const std::string& name)

View File

@ -28,6 +28,7 @@ class GameEngine
bool headless = false; bool headless = false;
McRogueFaceConfig config; McRogueFaceConfig config;
bool cleaned_up = false;
void testTimers(); void testTimers();
@ -50,6 +51,7 @@ public:
sf::RenderTarget* getRenderTargetPtr() { return render_target; } sf::RenderTarget* getRenderTargetPtr() { return render_target; }
void run(); void run();
void sUserInput(); void sUserInput();
void cleanup(); // Clean up Python references before destruction
int getFrame() { return currentFrame; } int getFrame() { return currentFrame; }
float getFrameTime() { return frameTime; } float getFrameTime() { return frameTime; }
sf::View getView() { return visible; } sf::View getView() { return visible; }

View File

@ -1,4 +1,6 @@
#pragma once #pragma once
#include "Python.h"
#include <memory>
class UIEntity; class UIEntity;
typedef struct { typedef struct {
@ -30,3 +32,103 @@ typedef struct {
PyObject_HEAD PyObject_HEAD
std::shared_ptr<UISprite> data; std::shared_ptr<UISprite> data;
} PyUISpriteObject; } PyUISpriteObject;
// Common Python method implementations for UIDrawable-derived classes
// These template functions provide shared functionality for Python bindings
// get_bounds method implementation (#89)
template<typename T>
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<typename T>
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<typename T>
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<PyObjectType>, METH_NOARGS, \
"Get bounding box as (x, y, width, height)"}, \
{"move", (PyCFunction)UIDrawable_move<PyObjectType>, METH_VARARGS, \
"Move by relative offset (dx, dy)"}, \
{"resize", (PyCFunction)UIDrawable_resize<PyObjectType>, METH_VARARGS, \
"Resize to new dimensions (width, height)"}
// Property getters/setters for visible and opacity
template<typename T>
static PyObject* UIDrawable_get_visible(T* self, void* closure)
{
return PyBool_FromLong(self->data->visible);
}
template<typename T>
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<typename T>
static PyObject* UIDrawable_get_opacity(T* self, void* closure)
{
return PyFloat_FromDouble(self->data->opacity);
}
template<typename T>
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<PyObjectType>, (setter)UIDrawable_set_visible<PyObjectType>, \
"Visibility flag", NULL}, \
{"opacity", (getter)UIDrawable_get_opacity<PyObjectType>, (setter)UIDrawable_set_opacity<PyObjectType>, \
"Opacity (0.0 = transparent, 1.0 = opaque)", NULL}
// UIEntity specializations are defined in UIEntity.cpp after UIEntity class is complete

View File

@ -4,7 +4,7 @@
#include "PyVector.h" #include "PyVector.h"
#include "PyFont.h" #include "PyFont.h"
#include "PyPositionHelper.h" #include "PyPositionHelper.h"
#include "UIDrawable_methods.h" // UIDrawable methods now in UIBase.h
#include <algorithm> #include <algorithm>
UICaption::UICaption() UICaption::UICaption()
@ -277,7 +277,7 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
{ {
using namespace mcrfpydef; 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; float x = 0.0f, y = 0.0f, outline = 0.0f;
char* text = NULL; char* text = NULL;
PyObject* font = NULL; PyObject* font = NULL;
@ -286,11 +286,48 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
PyObject* click_handler = NULL; PyObject* click_handler = NULL;
PyObject* pos_obj = NULL; PyObject* pos_obj = NULL;
// Try parsing all arguments with keywords // Handle different argument patterns
if (PyArg_ParseTupleAndKeywords(args, kwds, "|ffzOOOfOO", Py_ssize_t args_size = PyTuple_Size(args);
const_cast<char**>(keywords),
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<char**>(pos_keywords),
&pos, &text, &font, &fill_color, &outline_color, &outline, &click_handler, &x, &y))
{
return -1;
}
// 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<char**>(xy_keywords),
&x, &y, &text, &font, &fill_color, &outline_color, &outline, &click_handler, &pos_obj)) &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 was provided, it overrides x,y
if (pos_obj && pos_obj != Py_None) { if (pos_obj && pos_obj != Py_None) {
PyVectorObject* vec = PyVector::from_arg(pos_obj); PyVectorObject* vec = PyVector::from_arg(pos_obj);
@ -302,23 +339,19 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
y = vec->data.y; y = vec->data.y;
} }
} }
}
else { else {
PyErr_Clear(); // Pattern 2: (text, ...) with x, y as keywords
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|zffOOOfOO",
// Try alternative: first arg is pos tuple/Vector const_cast<char**>(keywords),
static const char* alt_keywords[] = { "pos", "text", "font", "fill_color", "outline_color", "outline", "click", nullptr }; &text, &x, &y, &font, &fill_color, &outline_color, &outline, &click_handler, &pos_obj))
PyObject* pos = NULL;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OzOOOfO",
const_cast<char**>(alt_keywords),
&pos, &text, &font, &fill_color, &outline_color, &outline, &click_handler))
{ {
return -1; return -1;
} }
// Parse position // If pos was provided, it overrides x,y
if (pos && pos != Py_None) { if (pos_obj && pos_obj != Py_None) {
PyVectorObject* vec = PyVector::from_arg(pos); PyVectorObject* vec = PyVector::from_arg(pos_obj);
if (!vec) { if (!vec) {
PyErr_SetString(PyExc_TypeError, "pos must be a Vector or tuple (x, y)"); PyErr_SetString(PyExc_TypeError, "pos must be a Vector or tuple (x, y)");
return -1; return -1;

View File

@ -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<typename T>
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<typename T>
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<typename T>
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<PyObjectType>, METH_NOARGS, \
"Get bounding box as (x, y, width, height)"}, \
{"move", (PyCFunction)UIDrawable_move<PyObjectType>, METH_VARARGS, \
"Move by relative offset (dx, dy)"}, \
{"resize", (PyCFunction)UIDrawable_resize<PyObjectType>, METH_VARARGS, \
"Resize to new dimensions (width, height)"}
// Common getsetters for UIDrawable properties
#define UIDRAWABLE_GETSETTERS \
{"visible", (getter)UIDrawable_get_visible<PyObjectType>, (setter)UIDrawable_set_visible<PyObjectType>, \
"Whether the object is visible", NULL}, \
{"opacity", (getter)UIDrawable_get_opacity<PyObjectType>, (setter)UIDrawable_set_opacity<PyObjectType>, \
"Opacity level (0.0 = transparent, 1.0 = opaque)", NULL}
// Visible property getter (new for #87)
template<typename T>
static PyObject* UIDrawable_get_visible(T* self, void* closure)
{
return PyBool_FromLong(self->data->visible);
}
// Visible property setter (new for #87)
template<typename T>
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<typename T>
static PyObject* UIDrawable_get_opacity(T* self, void* closure)
{
return PyFloat_FromDouble(self->data->opacity);
}
// Opacity property setter (new for #88)
template<typename T>
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>(PyUIEntityObject* self, void* closure)
{
return PyBool_FromLong(self->data->sprite.visible);
}
template<>
inline int UIDrawable_set_visible<PyUIEntityObject>(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>(PyUIEntityObject* self, void* closure)
{
return PyFloat_FromDouble(self->data->sprite.opacity);
}
template<>
inline int UIDrawable_set_opacity<PyUIEntityObject>(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>(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>(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>(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;
}

View File

@ -5,7 +5,8 @@
#include "PyObjectUtils.h" #include "PyObjectUtils.h"
#include "PyVector.h" #include "PyVector.h"
#include "PyPositionHelper.h" #include "PyPositionHelper.h"
#include "UIDrawable_methods.h" // UIDrawable methods now in UIBase.h
#include "UIEntityPyMethods.h"
UIEntity::UIEntity() 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}, {"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}, {"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}, {"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 */ {NULL} /* Sentinel */
}; };

View File

@ -51,6 +51,11 @@ public:
bool setProperty(const std::string& name, int value); bool setProperty(const std::string& name, int value);
bool getProperty(const std::string& name, float& value) const; 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* at(PyUIEntityObject* self, PyObject* o);
static PyObject* index(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored)); static PyObject* index(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored));
static PyObject* die(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored)); static PyObject* die(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored));

48
src/UIEntityPyMethods.h Normal file
View File

@ -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;
}

View File

@ -7,7 +7,7 @@
#include "UIGrid.h" #include "UIGrid.h"
#include "McRFPy_API.h" #include "McRFPy_API.h"
#include "PyPositionHelper.h" #include "PyPositionHelper.h"
#include "UIDrawable_methods.h" // UIDrawable methods now in UIBase.h
UIDrawable* UIFrame::click_at(sf::Vector2f point) UIDrawable* UIFrame::click_at(sf::Vector2f point)
{ {

View File

@ -3,7 +3,7 @@
#include "McRFPy_API.h" #include "McRFPy_API.h"
#include "PyPositionHelper.h" #include "PyPositionHelper.h"
#include <algorithm> #include <algorithm>
#include "UIDrawable_methods.h" // UIDrawable methods now in UIBase.h
UIGrid::UIGrid() UIGrid::UIGrid()
: grid_x(0), grid_y(0), zoom(1.0f), center_x(0.0f), center_y(0.0f), ptex(nullptr) : grid_x(0), grid_y(0), zoom(1.0f), center_x(0.0f), center_y(0.0f), ptex(nullptr)

View File

@ -2,7 +2,7 @@
#include "GameEngine.h" #include "GameEngine.h"
#include "PyVector.h" #include "PyVector.h"
#include "PyPositionHelper.h" #include "PyPositionHelper.h"
#include "UIDrawable_methods.h" // UIDrawable methods now in UIBase.h
UIDrawable* UISprite::click_at(sf::Vector2f point) UIDrawable* UISprite::click_at(sf::Vector2f point)
{ {