McRogueFace/src/UIEntity.cpp
John McCardle cf67c995f6 refactor: implement PyArgHelpers for standardized Python argument parsing
This major refactoring standardizes how position, size, and other arguments
are parsed across all UI components. PyArgHelpers provides consistent handling
for various argument patterns:

- Position as (x, y) tuple or separate x, y args
- Size as (w, h) tuple or separate width, height args
- Grid position and size with proper validation
- Color parsing with PyColorObject support

Changes across UI components:
- UICaption: Migrated to PyArgHelpers, improved resize() for future multiline support
- UIFrame: Uses standardized position parsing
- UISprite: Consistent position handling
- UIGrid: Grid-specific position/size helpers
- UIEntity: Unified argument parsing

Also includes:
- Improved error messages for type mismatches (int or float accepted)
- Reduced code duplication across constructors
- Better handling of keyword/positional argument conflicts
- Maintains backward compatibility with existing API

This addresses the inconsistent argument handling patterns discovered during
the inheritance hierarchy work and prepares for Phase 7 documentation.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-08 08:49:26 -04:00

469 lines
17 KiB
C++

#include "UIEntity.h"
#include "UIGrid.h"
#include "McRFPy_API.h"
#include <algorithm>
#include "PyObjectUtils.h"
#include "PyVector.h"
#include "PyArgHelpers.h"
// UIDrawable methods now in UIBase.h
#include "UIEntityPyMethods.h"
UIEntity::UIEntity()
: self(nullptr), grid(nullptr), position(0.0f, 0.0f)
{
// Initialize sprite with safe defaults (sprite has its own safe constructor now)
// gridstate vector starts empty since we don't know grid dimensions
}
UIEntity::UIEntity(UIGrid& grid)
: gridstate(grid.grid_x * grid.grid_y)
{
}
PyObject* UIEntity::at(PyUIEntityObject* self, PyObject* o) {
int x, y;
if (!PyArg_ParseTuple(o, "ii", &x, &y)) {
PyErr_SetString(PyExc_TypeError, "UIEntity.at requires two integer arguments: (x, y)");
return NULL;
}
if (self->data->grid == NULL) {
PyErr_SetString(PyExc_ValueError, "Entity cannot access surroundings because it is not associated with a grid");
return NULL;
}
/*
PyUIGridPointStateObject* obj = (PyUIGridPointStateObject*)((&mcrfpydef::PyUIGridPointStateType)->tp_alloc(&mcrfpydef::PyUIGridPointStateType, 0));
*/
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "GridPointState");
auto obj = (PyUIGridPointStateObject*)type->tp_alloc(type, 0);
//auto target = std::static_pointer_cast<UIEntity>(target);
obj->data = &(self->data->gridstate[y + self->data->grid->grid_x * x]);
obj->grid = self->data->grid;
obj->entity = self->data;
return (PyObject*)obj;
}
PyObject* UIEntity::index(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored)) {
// Check if entity has an associated grid
if (!self->data || !self->data->grid) {
PyErr_SetString(PyExc_RuntimeError, "Entity is not associated with a grid");
return NULL;
}
// Get the grid's entity collection
auto entities = self->data->grid->entities;
if (!entities) {
PyErr_SetString(PyExc_RuntimeError, "Grid has no entity collection");
return NULL;
}
// Find this entity in the collection
int index = 0;
for (auto it = entities->begin(); it != entities->end(); ++it, ++index) {
if (it->get() == self->data.get()) {
return PyLong_FromLong(index);
}
}
// Entity not found in its grid's collection
PyErr_SetString(PyExc_ValueError, "Entity not found in its grid's entity collection");
return NULL;
}
int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
// Try parsing with PyArgHelpers for grid position
int arg_idx = 0;
auto grid_pos_result = PyArgHelpers::parseGridPosition(args, kwds, &arg_idx);
// Default values
float grid_x = 0.0f, grid_y = 0.0f;
int sprite_index = 0;
PyObject* texture = nullptr;
PyObject* grid_obj = nullptr;
// Case 1: Got grid position from helpers (tuple format)
if (grid_pos_result.valid) {
grid_x = grid_pos_result.grid_x;
grid_y = grid_pos_result.grid_y;
// Parse remaining arguments
static const char* remaining_keywords[] = {
"texture", "sprite_index", "grid", nullptr
};
// Create new tuple with remaining args
Py_ssize_t total_args = PyTuple_Size(args);
PyObject* remaining_args = PyTuple_GetSlice(args, arg_idx, total_args);
if (!PyArg_ParseTupleAndKeywords(remaining_args, kwds, "|OiO",
const_cast<char**>(remaining_keywords),
&texture, &sprite_index, &grid_obj)) {
Py_DECREF(remaining_args);
if (grid_pos_result.error) PyErr_SetString(PyExc_TypeError, grid_pos_result.error);
return -1;
}
Py_DECREF(remaining_args);
}
// Case 2: Traditional format
else {
PyErr_Clear(); // Clear any errors from helpers
static const char* keywords[] = {
"grid_x", "grid_y", "texture", "sprite_index", "grid", "grid_pos", nullptr
};
PyObject* grid_pos_obj = nullptr;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffOiOO",
const_cast<char**>(keywords),
&grid_x, &grid_y, &texture, &sprite_index,
&grid_obj, &grid_pos_obj)) {
return -1;
}
// Handle grid_pos keyword override
if (grid_pos_obj && grid_pos_obj != Py_None) {
if (PyTuple_Check(grid_pos_obj) && PyTuple_Size(grid_pos_obj) == 2) {
PyObject* x_val = PyTuple_GetItem(grid_pos_obj, 0);
PyObject* y_val = PyTuple_GetItem(grid_pos_obj, 1);
if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) &&
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
grid_x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
grid_y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
}
} else {
PyErr_SetString(PyExc_TypeError, "grid_pos must be a tuple (x, y)");
return -1;
}
}
}
// check types for texture
//
// Set Texture - allow None or use default
//
std::shared_ptr<PyTexture> texture_ptr = nullptr;
if (texture != NULL && texture != Py_None && !PyObject_IsInstance(texture, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))){
PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None");
return -1;
} else if (texture != NULL && texture != Py_None) {
auto pytexture = (PyTextureObject*)texture;
texture_ptr = pytexture->data;
} else {
// Use default texture when None or not provided
texture_ptr = McRFPy_API::default_texture;
}
// Allow creation without texture for testing purposes
// if (!texture_ptr) {
// PyErr_SetString(PyExc_RuntimeError, "No texture provided and no default texture available");
// return -1;
// }
if (grid_obj != NULL && !PyObject_IsInstance(grid_obj, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
PyErr_SetString(PyExc_TypeError, "grid must be a mcrfpy.Grid instance");
return -1;
}
if (grid_obj == NULL)
self->data = std::make_shared<UIEntity>();
else
self->data = std::make_shared<UIEntity>(*((PyUIGridObject*)grid_obj)->data);
// Store reference to Python object
self->data->self = (PyObject*)self;
Py_INCREF(self);
// TODO - PyTextureObjects and IndexTextures are a little bit of a mess with shared/unshared pointers
if (texture_ptr) {
self->data->sprite = UISprite(texture_ptr, sprite_index, sf::Vector2f(0,0), 1.0);
} else {
// Create an empty sprite for testing
self->data->sprite = UISprite();
}
// Set position using grid coordinates
self->data->position = sf::Vector2f(grid_x, grid_y);
if (grid_obj != NULL) {
PyUIGridObject* pygrid = (PyUIGridObject*)grid_obj;
self->data->grid = pygrid->data;
// todone - on creation of Entity with Grid assignment, also append it to the entity list
pygrid->data->entities->push_back(self->data);
}
return 0;
}
PyObject* UIEntity::get_spritenumber(PyUIEntityObject* self, void* closure) {
return PyLong_FromDouble(self->data->sprite.getSpriteIndex());
}
PyObject* sfVector2f_to_PyObject(sf::Vector2f vec) {
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
auto obj = (PyVectorObject*)type->tp_alloc(type, 0);
if (obj) {
obj->data = vec;
}
return (PyObject*)obj;
}
PyObject* sfVector2i_to_PyObject(sf::Vector2i vec) {
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
auto obj = (PyVectorObject*)type->tp_alloc(type, 0);
if (obj) {
obj->data = sf::Vector2f(static_cast<float>(vec.x), static_cast<float>(vec.y));
}
return (PyObject*)obj;
}
sf::Vector2f PyObject_to_sfVector2f(PyObject* obj) {
PyVectorObject* vec = PyVector::from_arg(obj);
if (!vec) {
// PyVector::from_arg already set the error
return sf::Vector2f(0, 0);
}
return vec->data;
}
sf::Vector2i PyObject_to_sfVector2i(PyObject* obj) {
PyVectorObject* vec = PyVector::from_arg(obj);
if (!vec) {
// PyVector::from_arg already set the error
return sf::Vector2i(0, 0);
}
return sf::Vector2i(static_cast<int>(vec->data.x), static_cast<int>(vec->data.y));
}
// TODO - deprecate / remove this helper
PyObject* UIGridPointState_to_PyObject(const UIGridPointState& state) {
// This function is incomplete - it creates an empty object without setting state data
// Should use PyObjectUtils::createGridPointState() instead
return PyObjectUtils::createPyObjectGeneric("GridPointState");
}
PyObject* UIGridPointStateVector_to_PyList(const std::vector<UIGridPointState>& vec) {
PyObject* list = PyList_New(vec.size());
if (!list) return PyErr_NoMemory();
for (size_t i = 0; i < vec.size(); ++i) {
PyObject* obj = UIGridPointState_to_PyObject(vec[i]);
if (!obj) { // Cleanup on failure
Py_DECREF(list);
return NULL;
}
PyList_SET_ITEM(list, i, obj); // This steals a reference to obj
}
return list;
}
PyObject* UIEntity::get_position(PyUIEntityObject* self, void* closure) {
if (reinterpret_cast<long>(closure) == 0) {
return sfVector2f_to_PyObject(self->data->position);
} else {
// Return integer-cast position for grid coordinates
sf::Vector2i int_pos(static_cast<int>(self->data->position.x),
static_cast<int>(self->data->position.y));
return sfVector2i_to_PyObject(int_pos);
}
}
int UIEntity::set_position(PyUIEntityObject* self, PyObject* value, void* closure) {
if (reinterpret_cast<long>(closure) == 0) {
sf::Vector2f vec = PyObject_to_sfVector2f(value);
if (PyErr_Occurred()) {
return -1; // Error already set by PyObject_to_sfVector2f
}
self->data->position = vec;
} else {
// For integer position, convert to float and set position
sf::Vector2i vec = PyObject_to_sfVector2i(value);
if (PyErr_Occurred()) {
return -1; // Error already set by PyObject_to_sfVector2i
}
self->data->position = sf::Vector2f(static_cast<float>(vec.x),
static_cast<float>(vec.y));
}
return 0;
}
PyObject* UIEntity::get_gridstate(PyUIEntityObject* self, void* closure) {
// Assuming a function to convert std::vector<UIGridPointState> to PyObject* list
return UIGridPointStateVector_to_PyList(self->data->gridstate);
}
int UIEntity::set_spritenumber(PyUIEntityObject* self, PyObject* value, void* closure) {
int val;
if (PyLong_Check(value))
val = PyLong_AsLong(value);
else
{
PyErr_SetString(PyExc_TypeError, "sprite_index must be an integer");
return -1;
}
//self->data->sprite.sprite_index = val;
self->data->sprite.setSpriteIndex(val); // todone - I don't like ".sprite.sprite" in this stack of UIEntity.UISprite.sf::Sprite
return 0;
}
PyObject* UIEntity::get_float_member(PyUIEntityObject* self, void* closure)
{
auto member_ptr = reinterpret_cast<long>(closure);
if (member_ptr == 0) // x
return PyFloat_FromDouble(self->data->position.x);
else if (member_ptr == 1) // y
return PyFloat_FromDouble(self->data->position.y);
else
{
PyErr_SetString(PyExc_AttributeError, "Invalid attribute");
return nullptr;
}
}
int UIEntity::set_float_member(PyUIEntityObject* self, PyObject* value, void* closure)
{
float val;
auto member_ptr = reinterpret_cast<long>(closure);
if (PyFloat_Check(value))
{
val = PyFloat_AsDouble(value);
}
else if (PyLong_Check(value))
{
val = PyLong_AsLong(value);
}
else
{
PyErr_SetString(PyExc_TypeError, "Position must be a number (int or float)");
return -1;
}
if (member_ptr == 0) // x
{
self->data->position.x = val;
}
else if (member_ptr == 1) // y
{
self->data->position.y = val;
}
return 0;
}
PyObject* UIEntity::die(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored))
{
// Check if entity has a grid
if (!self->data || !self->data->grid) {
Py_RETURN_NONE; // Entity not on a grid, nothing to do
}
// Remove entity from grid's entity list
auto grid = self->data->grid;
auto& entities = grid->entities;
// Find and remove this entity from the list
auto it = std::find_if(entities->begin(), entities->end(),
[self](const std::shared_ptr<UIEntity>& e) {
return e.get() == self->data.get();
});
if (it != entities->end()) {
entities->erase(it);
// Clear the grid reference
self->data->grid.reset();
}
Py_RETURN_NONE;
}
PyMethodDef UIEntity::methods[] = {
{"at", (PyCFunction)UIEntity::at, METH_O},
{"index", (PyCFunction)UIEntity::index, METH_NOARGS, "Return the index of this entity in its grid's entity collection"},
{"die", (PyCFunction)UIEntity::die, METH_NOARGS, "Remove this entity from its grid"},
{NULL, NULL, 0, NULL}
};
// Define the PyObjectType alias for the macros
typedef PyUIEntityObject PyObjectType;
// Combine base methods with entity-specific methods
PyMethodDef UIEntity_all_methods[] = {
UIDRAWABLE_METHODS,
{"at", (PyCFunction)UIEntity::at, METH_O},
{"index", (PyCFunction)UIEntity::index, METH_NOARGS, "Return the index of this entity in its grid's entity collection"},
{"die", (PyCFunction)UIEntity::die, METH_NOARGS, "Remove this entity from its grid"},
{NULL} // Sentinel
};
PyGetSetDef UIEntity::getsetters[] = {
{"draw_pos", (getter)UIEntity::get_position, (setter)UIEntity::set_position, "Entity position (graphically)", (void*)0},
{"pos", (getter)UIEntity::get_position, (setter)UIEntity::set_position, "Entity position (integer grid coordinates)", (void*)1},
{"gridstate", (getter)UIEntity::get_gridstate, NULL, "Grid point states for the entity", NULL},
{"sprite_index", (getter)UIEntity::get_spritenumber, (setter)UIEntity::set_spritenumber, "Sprite index on the texture on the display", NULL},
{"sprite_number", (getter)UIEntity::get_spritenumber, (setter)UIEntity::set_spritenumber, "Sprite index (DEPRECATED: use sprite_index instead)", 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},
{"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},
{"name", (getter)UIEntity_get_name, (setter)UIEntity_set_name, "Name for finding elements", NULL},
{NULL} /* Sentinel */
};
PyObject* UIEntity::repr(PyUIEntityObject* self) {
std::ostringstream ss;
if (!self->data) ss << "<Entity (invalid internal object)>";
else {
auto ent = self->data;
ss << "<Entity (x=" << self->data->position.x << ", y=" << self->data->position.y << ", sprite_index=" << self->data->sprite.getSpriteIndex() <<
")>";
}
std::string repr_str = ss.str();
return PyUnicode_DecodeUTF8(repr_str.c_str(), repr_str.size(), "replace");
}
// Property system implementation for animations
bool UIEntity::setProperty(const std::string& name, float value) {
if (name == "x") {
position.x = value;
// Update sprite position based on grid position
// Note: This is a simplified version - actual grid-to-pixel conversion depends on grid properties
sprite.setPosition(sf::Vector2f(position.x, position.y));
return true;
}
else if (name == "y") {
position.y = value;
// Update sprite position based on grid position
sprite.setPosition(sf::Vector2f(position.x, position.y));
return true;
}
else if (name == "sprite_scale") {
sprite.setScale(sf::Vector2f(value, value));
return true;
}
return false;
}
bool UIEntity::setProperty(const std::string& name, int value) {
if (name == "sprite_index" || name == "sprite_number") {
sprite.setSpriteIndex(value);
return true;
}
return false;
}
bool UIEntity::getProperty(const std::string& name, float& value) const {
if (name == "x") {
value = position.x;
return true;
}
else if (name == "y") {
value = position.y;
return true;
}
else if (name == "sprite_scale") {
value = sprite.getScale().x; // Assuming uniform scale
return true;
}
return false;
}