Squashed commit of the following: [alpha_streamline_1]

the low-hanging fruit of pre-existing issues and standardizing the
Python interfaces

Special thanks to Claude Code, ~100k output tokens for this merge

    🤖 Generated with [Claude Code](https://claude.ai/code)
    Co-Authored-By: Claude <noreply@anthropic.com>

commit 99f301e3a0
Author: John McCardle <mccardle.john@gmail.com>
Date:   Sat Jul 5 16:25:32 2025 -0400

    Add position tuple support and pos property to UI elements

    closes #83, closes #84

    - Issue #83: Add position tuple support to constructors
      - Frame and Sprite now accept both (x, y) and ((x, y)) forms
      - Also accept Vector objects as position arguments
      - Caption and Entity already supported tuple/Vector forms
      - Uses PyVector::from_arg for flexible position parsing

    - Issue #84: Add pos property to Frame and Sprite
      - Added pos getter that returns a Vector
      - Added pos setter that accepts Vector or tuple
      - Provides consistency with Caption and Entity which already had pos properties
      - All UI elements now have a uniform way to get/set positions as Vectors

    Both features improve API consistency and make it easier to work with positions.

commit 2f2b488fb5
Author: John McCardle <mccardle.john@gmail.com>
Date:   Sat Jul 5 16:18:10 2025 -0400

    Standardize sprite_index property and add scale_x/scale_y to UISprite

    closes #81, closes #82

    - Issue #81: Standardized property name to sprite_index across UISprite and UIEntity
      - Added sprite_index as the primary property name
      - Kept sprite_number as a deprecated alias for backward compatibility
      - Updated repr() methods to use sprite_index
      - Updated animation system to recognize both names

    - Issue #82: Added scale_x and scale_y properties to UISprite
      - Enables non-uniform scaling of sprites
      - scale property still works for uniform scaling
      - Both properties work with the animation system

    All existing code using sprite_number continues to work due to backward compatibility.

commit 5a003a9aa5
Author: John McCardle <mccardle.john@gmail.com>
Date:   Sat Jul 5 16:09:52 2025 -0400

    Fix multiple low priority issues

    closes #12, closes #80, closes #95, closes #96, closes #99

    - Issue #12: Set tp_new to NULL for GridPoint and GridPointState to prevent instantiation from Python
    - Issue #80: Renamed Caption.size to Caption.font_size for semantic clarity
    - Issue #95: Fixed UICollection repr to show actual derived types instead of generic UIDrawable
    - Issue #96: Added extend() method to UICollection for API consistency with UIEntityCollection
    - Issue #99: Exposed read-only properties for Texture (sprite_width, sprite_height, sheet_width, sheet_height, sprite_count, source) and Font (family, source)

    All issues have corresponding tests that verify the fixes work correctly.

commit e5affaf317
Author: John McCardle <mccardle.john@gmail.com>
Date:   Sat Jul 5 15:50:09 2025 -0400

    Fix critical issues: script loading, entity types, and color properties

    - Issue #37: Fix Windows scripts subdirectory not checked
      - Updated executeScript() to use executable_path() from platform.h
      - Scripts now load correctly when working directory differs from executable

    - Issue #76: Fix UIEntityCollection returns wrong type
      - Updated UIEntityCollectionIter::next() to check for stored Python object
      - Derived Entity classes now preserve their type when retrieved from collections

    - Issue #9: Recreate RenderTexture when resized (already fixed)
      - Confirmed RenderTexture recreation already implemented in set_size() and set_float_member()
      - Uses 1.5x padding and 4096 max size limit

    - Issue #79: Fix Color r, g, b, a properties return None
      - Implemented get_member() and set_member() in PyColor.cpp
      - Color component properties now work correctly with proper validation

    - Additional fix: Grid.at() method signature
      - Changed from METH_O to METH_VARARGS to accept two arguments

    All fixes include comprehensive tests to verify functionality.

    closes #37, closes #76, closes #9, closes #79
This commit is contained in:
John McCardle 2025-07-05 17:30:49 -04:00
commit cd0bd5468b
41 changed files with 4212 additions and 34 deletions

View file

@ -90,8 +90,8 @@ void Animation::startEntity(UIEntity* target) {
}
}
else if constexpr (std::is_same_v<T, int>) {
// For entities, we might need to handle sprite_number differently
if (targetProperty == "sprite_number") {
// For entities, we might need to handle sprite_index differently
if (targetProperty == "sprite_index" || targetProperty == "sprite_number") {
startValue = target->sprite.getSpriteIndex();
}
}

View file

@ -313,12 +313,27 @@ void McRFPy_API::api_init(const McRogueFaceConfig& config, int argc, char** argv
void McRFPy_API::executeScript(std::string filename)
{
FILE* PScriptFile = fopen(filename.c_str(), "r");
std::filesystem::path script_path(filename);
// If the path is relative and the file doesn't exist, try resolving it relative to the executable
if (script_path.is_relative() && !std::filesystem::exists(script_path)) {
// Get the directory where the executable is located using platform-specific function
std::wstring exe_dir_w = executable_path();
std::filesystem::path exe_dir(exe_dir_w);
// Try the script path relative to the executable directory
std::filesystem::path resolved_path = exe_dir / script_path;
if (std::filesystem::exists(resolved_path)) {
script_path = resolved_path;
}
}
FILE* PScriptFile = fopen(script_path.string().c_str(), "r");
if(PScriptFile) {
std::cout << "Before PyRun_SimpleFile" << std::endl;
PyRun_SimpleFile(PScriptFile, filename.c_str());
std::cout << "After PyRun_SimpleFile" << std::endl;
PyRun_SimpleFile(PScriptFile, script_path.string().c_str());
fclose(PScriptFile);
} else {
std::cout << "Failed to open script: " << script_path.string() << std::endl;
}
}

View file

@ -133,13 +133,58 @@ PyObject* PyColor::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds)
PyObject* PyColor::get_member(PyObject* obj, void* closure)
{
// TODO
return Py_None;
PyColorObject* self = (PyColorObject*)obj;
long member = (long)closure;
switch (member) {
case 0: // r
return PyLong_FromLong(self->data.r);
case 1: // g
return PyLong_FromLong(self->data.g);
case 2: // b
return PyLong_FromLong(self->data.b);
case 3: // a
return PyLong_FromLong(self->data.a);
default:
PyErr_SetString(PyExc_AttributeError, "Invalid color member");
return NULL;
}
}
int PyColor::set_member(PyObject* obj, PyObject* value, void* closure)
{
// TODO
PyColorObject* self = (PyColorObject*)obj;
long member = (long)closure;
if (!PyLong_Check(value)) {
PyErr_SetString(PyExc_TypeError, "Color values must be integers");
return -1;
}
long val = PyLong_AsLong(value);
if (val < 0 || val > 255) {
PyErr_SetString(PyExc_ValueError, "Color values must be between 0 and 255");
return -1;
}
switch (member) {
case 0: // r
self->data.r = static_cast<sf::Uint8>(val);
break;
case 1: // g
self->data.g = static_cast<sf::Uint8>(val);
break;
case 2: // b
self->data.b = static_cast<sf::Uint8>(val);
break;
case 3: // a
self->data.a = static_cast<sf::Uint8>(val);
break;
default:
PyErr_SetString(PyExc_AttributeError, "Invalid color member");
return -1;
}
return 0;
}

View file

@ -61,3 +61,19 @@ PyObject* PyFont::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds)
{
return (PyObject*)type->tp_alloc(type, 0);
}
PyObject* PyFont::get_family(PyFontObject* self, void* closure)
{
return PyUnicode_FromString(self->data->font.getInfo().family.c_str());
}
PyObject* PyFont::get_source(PyFontObject* self, void* closure)
{
return PyUnicode_FromString(self->data->source.c_str());
}
PyGetSetDef PyFont::getsetters[] = {
{"family", (getter)PyFont::get_family, NULL, "Font family name", NULL},
{"source", (getter)PyFont::get_source, NULL, "Source filename of the font", NULL},
{NULL} // Sentinel
};

View file

@ -21,6 +21,12 @@ public:
static Py_hash_t hash(PyObject*);
static int init(PyFontObject*, PyObject*, PyObject*);
static PyObject* pynew(PyTypeObject* type, PyObject* args=NULL, PyObject* kwds=NULL);
// Getters for properties
static PyObject* get_family(PyFontObject* self, void* closure);
static PyObject* get_source(PyFontObject* self, void* closure);
static PyGetSetDef getsetters[];
};
namespace mcrfpydef {
@ -33,6 +39,7 @@ namespace mcrfpydef {
//.tp_hash = PyFont::hash,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("SFML Font Object"),
.tp_getset = PyFont::getsetters,
//.tp_base = &PyBaseObject_Type,
.tp_init = (initproc)PyFont::init,
.tp_new = PyType_GenericNew, //PyFont::pynew,

View file

@ -79,3 +79,43 @@ PyObject* PyTexture::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds)
{
return (PyObject*)type->tp_alloc(type, 0);
}
PyObject* PyTexture::get_sprite_width(PyTextureObject* self, void* closure)
{
return PyLong_FromLong(self->data->sprite_width);
}
PyObject* PyTexture::get_sprite_height(PyTextureObject* self, void* closure)
{
return PyLong_FromLong(self->data->sprite_height);
}
PyObject* PyTexture::get_sheet_width(PyTextureObject* self, void* closure)
{
return PyLong_FromLong(self->data->sheet_width);
}
PyObject* PyTexture::get_sheet_height(PyTextureObject* self, void* closure)
{
return PyLong_FromLong(self->data->sheet_height);
}
PyObject* PyTexture::get_sprite_count(PyTextureObject* self, void* closure)
{
return PyLong_FromLong(self->data->getSpriteCount());
}
PyObject* PyTexture::get_source(PyTextureObject* self, void* closure)
{
return PyUnicode_FromString(self->data->source.c_str());
}
PyGetSetDef PyTexture::getsetters[] = {
{"sprite_width", (getter)PyTexture::get_sprite_width, NULL, "Width of each sprite in pixels", NULL},
{"sprite_height", (getter)PyTexture::get_sprite_height, NULL, "Height of each sprite in pixels", NULL},
{"sheet_width", (getter)PyTexture::get_sheet_width, NULL, "Number of sprite columns in the texture", NULL},
{"sheet_height", (getter)PyTexture::get_sheet_height, NULL, "Number of sprite rows in the texture", NULL},
{"sprite_count", (getter)PyTexture::get_sprite_count, NULL, "Total number of sprites in the texture", NULL},
{"source", (getter)PyTexture::get_source, NULL, "Source filename of the texture", NULL},
{NULL} // Sentinel
};

View file

@ -26,6 +26,16 @@ public:
static Py_hash_t hash(PyObject*);
static int init(PyTextureObject*, PyObject*, PyObject*);
static PyObject* pynew(PyTypeObject* type, PyObject* args=NULL, PyObject* kwds=NULL);
// Getters for properties
static PyObject* get_sprite_width(PyTextureObject* self, void* closure);
static PyObject* get_sprite_height(PyTextureObject* self, void* closure);
static PyObject* get_sheet_width(PyTextureObject* self, void* closure);
static PyObject* get_sheet_height(PyTextureObject* self, void* closure);
static PyObject* get_sprite_count(PyTextureObject* self, void* closure);
static PyObject* get_source(PyTextureObject* self, void* closure);
static PyGetSetDef getsetters[];
};
namespace mcrfpydef {
@ -38,6 +48,7 @@ namespace mcrfpydef {
.tp_hash = PyTexture::hash,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("SFML Texture Object"),
.tp_getset = PyTexture::getsetters,
//.tp_base = &PyBaseObject_Type,
.tp_init = (initproc)PyTexture::init,
.tp_new = PyType_GenericNew, //PyTexture::pynew,

View file

@ -197,7 +197,7 @@ PyGetSetDef UICaption::getsetters[] = {
{"outline_color", (getter)UICaption::get_color_member, (setter)UICaption::set_color_member, "Outline color of the text", (void*)1},
//{"children", (getter)PyUIFrame_get_children, NULL, "UICollection of objects on top of this one", NULL},
{"text", (getter)UICaption::get_text, (setter)UICaption::set_text, "The text displayed", NULL},
{"size", (getter)UICaption::get_float_member, (setter)UICaption::set_float_member, "Text size (integer) in points", (void*)5},
{"font_size", (getter)UICaption::get_float_member, (setter)UICaption::set_float_member, "Font size (integer) in points", (void*)5},
{"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UICAPTION},
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UICAPTION},
{NULL}
@ -314,7 +314,7 @@ bool UICaption::setProperty(const std::string& name, float value) {
text.setPosition(sf::Vector2f(text.getPosition().x, value));
return true;
}
else if (name == "size") {
else if (name == "font_size" || name == "size") { // Support both for backward compatibility
text.setCharacterSize(static_cast<unsigned int>(value));
return true;
}
@ -406,7 +406,7 @@ bool UICaption::getProperty(const std::string& name, float& value) const {
value = text.getPosition().y;
return true;
}
else if (name == "size") {
else if (name == "font_size" || name == "size") { // Support both for backward compatibility
value = static_cast<float>(text.getCharacterSize());
return true;
}

View file

@ -615,6 +615,88 @@ PyObject* UICollection::append(PyUICollectionObject* self, PyObject* o)
return Py_None;
}
PyObject* UICollection::extend(PyUICollectionObject* self, PyObject* iterable)
{
// Accept any iterable of UIDrawable objects
PyObject* iterator = PyObject_GetIter(iterable);
if (iterator == NULL) {
PyErr_SetString(PyExc_TypeError, "UICollection.extend requires an iterable");
return NULL;
}
// Ensure module is initialized
if (!McRFPy_API::mcrf_module) {
Py_DECREF(iterator);
PyErr_SetString(PyExc_RuntimeError, "mcrfpy module not initialized");
return NULL;
}
// Get current highest z_index
int current_z_index = 0;
if (!self->data->empty()) {
current_z_index = self->data->back()->z_index;
}
PyObject* item;
while ((item = PyIter_Next(iterator)) != NULL) {
// Check if item is a UIDrawable subclass
if (!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")) &&
!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")) &&
!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")) &&
!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid")))
{
Py_DECREF(item);
Py_DECREF(iterator);
PyErr_SetString(PyExc_TypeError, "All items must be Frame, Caption, Sprite, or Grid objects");
return NULL;
}
// Increment z_index for each new element
if (current_z_index <= INT_MAX - 10) {
current_z_index += 10;
} else {
current_z_index = INT_MAX;
}
// Add the item based on its type
if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) {
PyUIFrameObject* frame = (PyUIFrameObject*)item;
frame->data->z_index = current_z_index;
self->data->push_back(frame->data);
}
else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) {
PyUICaptionObject* caption = (PyUICaptionObject*)item;
caption->data->z_index = current_z_index;
self->data->push_back(caption->data);
}
else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) {
PyUISpriteObject* sprite = (PyUISpriteObject*)item;
sprite->data->z_index = current_z_index;
self->data->push_back(sprite->data);
}
else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
PyUIGridObject* grid = (PyUIGridObject*)item;
grid->data->z_index = current_z_index;
self->data->push_back(grid->data);
}
Py_DECREF(item);
}
Py_DECREF(iterator);
// Check if iteration ended due to an error
if (PyErr_Occurred()) {
return NULL;
}
// Mark scene as needing resort after adding elements
McRFPy_API::markSceneNeedsSort();
Py_INCREF(Py_None);
return Py_None;
}
PyObject* UICollection::remove(PyUICollectionObject* self, PyObject* o)
{
if (!PyLong_Check(o))
@ -734,7 +816,7 @@ PyObject* UICollection::count(PyUICollectionObject* self, PyObject* value) {
PyMethodDef UICollection::methods[] = {
{"append", (PyCFunction)UICollection::append, METH_O},
//{"extend", (PyCFunction)PyUICollection_extend, METH_O}, // TODO
{"extend", (PyCFunction)UICollection::extend, METH_O},
{"remove", (PyCFunction)UICollection::remove, METH_O},
{"index", (PyCFunction)UICollection::index_method, METH_O},
{"count", (PyCFunction)UICollection::count, METH_O},
@ -746,7 +828,47 @@ PyObject* UICollection::repr(PyUICollectionObject* self)
std::ostringstream ss;
if (!self->data) ss << "<UICollection (invalid internal object)>";
else {
ss << "<UICollection (" << self->data->size() << " child objects)>";
ss << "<UICollection (" << self->data->size() << " objects: ";
// Count each type
int frame_count = 0, caption_count = 0, sprite_count = 0, grid_count = 0, other_count = 0;
for (auto& item : *self->data) {
switch(item->derived_type()) {
case PyObjectsEnum::UIFRAME: frame_count++; break;
case PyObjectsEnum::UICAPTION: caption_count++; break;
case PyObjectsEnum::UISPRITE: sprite_count++; break;
case PyObjectsEnum::UIGRID: grid_count++; break;
default: other_count++; break;
}
}
// Build type summary
bool first = true;
if (frame_count > 0) {
ss << frame_count << " Frame" << (frame_count > 1 ? "s" : "");
first = false;
}
if (caption_count > 0) {
if (!first) ss << ", ";
ss << caption_count << " Caption" << (caption_count > 1 ? "s" : "");
first = false;
}
if (sprite_count > 0) {
if (!first) ss << ", ";
ss << sprite_count << " Sprite" << (sprite_count > 1 ? "s" : "");
first = false;
}
if (grid_count > 0) {
if (!first) ss << ", ";
ss << grid_count << " Grid" << (grid_count > 1 ? "s" : "");
first = false;
}
if (other_count > 0) {
if (!first) ss << ", ";
ss << other_count << " UIDrawable" << (other_count > 1 ? "s" : "");
}
ss << ")>";
}
std::string repr_str = ss.str();
return PyUnicode_DecodeUTF8(repr_str.c_str(), repr_str.size(), "replace");

View file

@ -28,6 +28,7 @@ public:
static PyObject* subscript(PyUICollectionObject* self, PyObject* key);
static int ass_subscript(PyUICollectionObject* self, PyObject* key, PyObject* value);
static PyObject* append(PyUICollectionObject* self, PyObject* o);
static PyObject* extend(PyUICollectionObject* self, PyObject* iterable);
static PyObject* remove(PyUICollectionObject* self, PyObject* o);
static PyObject* index_method(PyUICollectionObject* self, PyObject* value);
static PyObject* count(PyUICollectionObject* self, PyObject* value);

View file

@ -119,6 +119,10 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
else
self->data = std::make_shared<UIEntity>(*((PyUIGridObject*)grid)->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
self->data->sprite = UISprite(texture_ptr, sprite_index, sf::Vector2f(0,0), 1.0);
self->data->position = pos_result->data;
@ -250,7 +254,8 @@ 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_number", (getter)UIEntity::get_spritenumber, (setter)UIEntity::set_spritenumber, "Sprite number (index) on the texture on the display", 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 on the texture on the display (deprecated: use sprite_index)", NULL},
{NULL} /* Sentinel */
};
@ -259,7 +264,7 @@ PyObject* UIEntity::repr(PyUIEntityObject* self) {
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_number=" << self->data->sprite.getSpriteIndex() <<
ss << "<Entity (x=" << self->data->position.x << ", y=" << self->data->position.y << ", sprite_index=" << self->data->sprite.getSpriteIndex() <<
")>";
}
std::string repr_str = ss.str();
@ -291,7 +296,7 @@ bool UIEntity::setProperty(const std::string& name, float value) {
}
bool UIEntity::setProperty(const std::string& name, int value) {
if (name == "sprite_number") {
if (name == "sprite_index" || name == "sprite_number") {
sprite.setSpriteIndex(value);
return true;
}

View file

@ -35,7 +35,7 @@ static PyObject* UIGridPointStateVector_to_PyList(const std::vector<UIGridPointS
class UIEntity//: public UIDrawable
{
public:
//PyObject* self;
PyObject* self = nullptr; // Reference to the Python object (if created from Python)
std::shared_ptr<UIGrid> grid;
std::vector<UIGridPointState> gridstate;
UISprite sprite;

View file

@ -1,6 +1,7 @@
#include "UIFrame.h"
#include "UICollection.h"
#include "GameEngine.h"
#include "PyVector.h"
UIDrawable* UIFrame::click_at(sf::Vector2f point)
{
@ -214,6 +215,28 @@ int UIFrame::set_color_member(PyUIFrameObject* self, PyObject* value, void* clos
return 0;
}
PyObject* UIFrame::get_pos(PyUIFrameObject* self, void* closure)
{
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
auto obj = (PyVectorObject*)type->tp_alloc(type, 0);
if (obj) {
auto pos = self->data->box.getPosition();
obj->data = sf::Vector2f(pos.x, pos.y);
}
return (PyObject*)obj;
}
int UIFrame::set_pos(PyUIFrameObject* self, PyObject* value, void* closure)
{
PyVectorObject* vec = PyVector::from_arg(value);
if (!vec) {
PyErr_SetString(PyExc_TypeError, "pos must be a Vector or convertible to Vector");
return -1;
}
self->data->box.setPosition(vec->data);
return 0;
}
PyGetSetDef UIFrame::getsetters[] = {
{"x", (getter)UIFrame::get_float_member, (setter)UIFrame::set_float_member, "X coordinate of top-left corner", (void*)0},
{"y", (getter)UIFrame::get_float_member, (setter)UIFrame::set_float_member, "Y coordinate of top-left corner", (void*)1},
@ -225,6 +248,7 @@ PyGetSetDef UIFrame::getsetters[] = {
{"children", (getter)UIFrame::get_children, NULL, "UICollection of objects on top of this one", NULL},
{"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UIFRAME},
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UIFRAME},
{"pos", (getter)UIFrame::get_pos, (setter)UIFrame::set_pos, "Position as a Vector", NULL},
{NULL}
};
@ -256,9 +280,29 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
PyObject* fill_color = 0;
PyObject* outline_color = 0;
// First try to parse as (x, y, w, h, ...)
if (!PyArg_ParseTupleAndKeywords(args, kwds, "ffff|OOf", const_cast<char**>(keywords), &x, &y, &w, &h, &fill_color, &outline_color, &outline))
{
return -1;
PyErr_Clear(); // Clear the error
// Try to parse as ((x,y), w, h, ...) or (Vector, w, h, ...)
PyObject* pos_obj = nullptr;
const char* alt_keywords[] = { "pos", "w", "h", "fill_color", "outline_color", "outline", nullptr };
if (!PyArg_ParseTupleAndKeywords(args, kwds, "Off|OOf", const_cast<char**>(alt_keywords),
&pos_obj, &w, &h, &fill_color, &outline_color, &outline))
{
return -1;
}
// Convert position argument to x, y
PyVectorObject* vec = PyVector::from_arg(pos_obj);
if (!vec) {
PyErr_SetString(PyExc_TypeError, "First argument must be a tuple (x, y) or Vector when not providing x, y separately");
return -1;
}
x = vec->data.x;
y = vec->data.y;
}
self->data->box.setPosition(sf::Vector2f(x, y));

View file

@ -40,6 +40,8 @@ public:
static int set_float_member(PyUIFrameObject* self, PyObject* value, void* closure);
static PyObject* get_color_member(PyUIFrameObject* self, void* closure);
static int set_color_member(PyUIFrameObject* self, PyObject* value, void* closure);
static PyObject* get_pos(PyUIFrameObject* self, void* closure);
static int set_pos(PyUIFrameObject* self, PyObject* value, void* closure);
static PyGetSetDef getsetters[];
static PyObject* repr(PyUIFrameObject* self);
static int init(PyUIFrameObject* self, PyObject* args, PyObject* kwds);

View file

@ -347,6 +347,18 @@ int UIGrid::set_size(PyUIGridObject* self, PyObject* value, void* closure) {
return -1;
}
self->data->box.setSize(sf::Vector2f(w, h));
// Recreate renderTexture with new size to avoid rendering issues
// Add some padding to handle zoom and ensure we don't cut off content
unsigned int tex_width = static_cast<unsigned int>(w * 1.5f);
unsigned int tex_height = static_cast<unsigned int>(h * 1.5f);
// Clamp to reasonable maximum to avoid GPU memory issues
tex_width = std::min(tex_width, 4096u);
tex_height = std::min(tex_height, 4096u);
self->data->renderTexture.create(tex_width, tex_height);
return 0;
}
@ -411,9 +423,25 @@ int UIGrid::set_float_member(PyUIGridObject* self, PyObject* value, void* closur
else if (member_ptr == 1) // y
self->data->box.setPosition(self->data->box.getPosition().x, val);
else if (member_ptr == 2) // w
{
self->data->box.setSize(sf::Vector2f(val, self->data->box.getSize().y));
// Recreate renderTexture when width changes
unsigned int tex_width = static_cast<unsigned int>(val * 1.5f);
unsigned int tex_height = static_cast<unsigned int>(self->data->box.getSize().y * 1.5f);
tex_width = std::min(tex_width, 4096u);
tex_height = std::min(tex_height, 4096u);
self->data->renderTexture.create(tex_width, tex_height);
}
else if (member_ptr == 3) // h
{
self->data->box.setSize(sf::Vector2f(self->data->box.getSize().x, val));
// Recreate renderTexture when height changes
unsigned int tex_width = static_cast<unsigned int>(self->data->box.getSize().x * 1.5f);
unsigned int tex_height = static_cast<unsigned int>(val * 1.5f);
tex_width = std::min(tex_width, 4096u);
tex_height = std::min(tex_height, 4096u);
self->data->renderTexture.create(tex_width, tex_height);
}
else if (member_ptr == 4) // center_x
self->data->center_x = val;
else if (member_ptr == 5) // center_y
@ -473,7 +501,7 @@ PyObject* UIGrid::py_at(PyUIGridObject* self, PyObject* o)
}
PyMethodDef UIGrid::methods[] = {
{"at", (PyCFunction)UIGrid::py_at, METH_O},
{"at", (PyCFunction)UIGrid::py_at, METH_VARARGS},
{NULL, NULL, 0, NULL}
};
@ -571,7 +599,13 @@ PyObject* UIEntityCollectionIter::next(PyUIEntityCollectionIterObject* self)
std::advance(l_begin, self->index-1);
auto target = *l_begin;
// Create and return a Python Entity object
// Return the stored Python object if it exists (preserves derived types)
if (target->self != nullptr) {
Py_INCREF(target->self);
return target->self;
}
// Otherwise create and return a new Python Entity object
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity");
auto o = (PyUIEntityObject*)type->tp_alloc(type, 0);
auto p = std::static_pointer_cast<UIEntity>(target);
@ -612,17 +646,198 @@ PyObject* UIEntityCollection::getitem(PyUIEntityCollectionObject* self, Py_ssize
auto l_begin = (*vec).begin();
std::advance(l_begin, index);
auto target = *l_begin; //auto target = (*vec)[index];
//RET_PY_INSTANCE(target);
// construct and return an entity object that points directly into the UIGrid's entity vector
//PyUIEntityObject* o = (PyUIEntityObject*)((&PyUIEntityType)->tp_alloc(&PyUIEntityType, 0));
// If the entity has a stored Python object reference, return that to preserve derived class
if (target->self != nullptr) {
Py_INCREF(target->self);
return target->self;
}
// Otherwise, create a new base Entity object
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity");
auto o = (PyUIEntityObject*)type->tp_alloc(type, 0);
auto p = std::static_pointer_cast<UIEntity>(target);
o->data = p;
return (PyObject*)o;
return NULL;
}
int UIEntityCollection::setitem(PyUIEntityCollectionObject* self, Py_ssize_t index, PyObject* value) {
auto list = self->data.get();
if (!list) {
PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer");
return -1;
}
// Handle negative indexing
while (index < 0) index += list->size();
// Bounds check
if (index >= list->size()) {
PyErr_SetString(PyExc_IndexError, "EntityCollection assignment index out of range");
return -1;
}
// Get iterator to the target position
auto it = list->begin();
std::advance(it, index);
// Handle deletion
if (value == NULL) {
// Clear grid reference from the entity being removed
(*it)->grid = nullptr;
list->erase(it);
return 0;
}
// Type checking - must be an Entity
if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"))) {
PyErr_SetString(PyExc_TypeError, "EntityCollection can only contain Entity objects");
return -1;
}
// Get the C++ object from the Python object
PyUIEntityObject* entity = (PyUIEntityObject*)value;
if (!entity->data) {
PyErr_SetString(PyExc_RuntimeError, "Invalid Entity object");
return -1;
}
// Clear grid reference from the old entity
(*it)->grid = nullptr;
// Replace the element and set grid reference
*it = entity->data;
entity->data->grid = self->grid;
return 0;
}
int UIEntityCollection::contains(PyUIEntityCollectionObject* self, PyObject* value) {
auto list = self->data.get();
if (!list) {
PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer");
return -1;
}
// Type checking - must be an Entity
if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"))) {
// Not an Entity, so it can't be in the collection
return 0;
}
// Get the C++ object from the Python object
PyUIEntityObject* entity = (PyUIEntityObject*)value;
if (!entity->data) {
return 0;
}
// Search for the object by comparing C++ pointers
for (const auto& ent : *list) {
if (ent.get() == entity->data.get()) {
return 1; // Found
}
}
return 0; // Not found
}
PyObject* UIEntityCollection::concat(PyUIEntityCollectionObject* self, PyObject* other) {
// Create a new Python list containing elements from both collections
if (!PySequence_Check(other)) {
PyErr_SetString(PyExc_TypeError, "can only concatenate sequence to EntityCollection");
return NULL;
}
Py_ssize_t self_len = self->data->size();
Py_ssize_t other_len = PySequence_Length(other);
if (other_len == -1) {
return NULL; // Error already set
}
PyObject* result_list = PyList_New(self_len + other_len);
if (!result_list) {
return NULL;
}
// Add all elements from self
Py_ssize_t idx = 0;
for (const auto& entity : *self->data) {
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity");
auto obj = (PyUIEntityObject*)type->tp_alloc(type, 0);
if (obj) {
obj->data = entity;
PyList_SET_ITEM(result_list, idx, (PyObject*)obj); // Steals reference
} else {
Py_DECREF(result_list);
Py_DECREF(type);
return NULL;
}
Py_DECREF(type);
idx++;
}
// Add all elements from other
for (Py_ssize_t i = 0; i < other_len; i++) {
PyObject* item = PySequence_GetItem(other, i);
if (!item) {
Py_DECREF(result_list);
return NULL;
}
PyList_SET_ITEM(result_list, self_len + i, item); // Steals reference
}
return result_list;
}
PyObject* UIEntityCollection::inplace_concat(PyUIEntityCollectionObject* self, PyObject* other) {
if (!PySequence_Check(other)) {
PyErr_SetString(PyExc_TypeError, "can only concatenate sequence to EntityCollection");
return NULL;
}
// First, validate ALL items in the sequence before modifying anything
Py_ssize_t other_len = PySequence_Length(other);
if (other_len == -1) {
return NULL; // Error already set
}
// Validate all items first
for (Py_ssize_t i = 0; i < other_len; i++) {
PyObject* item = PySequence_GetItem(other, i);
if (!item) {
return NULL;
}
// Type check
if (!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"))) {
Py_DECREF(item);
PyErr_Format(PyExc_TypeError,
"EntityCollection can only contain Entity objects; "
"got %s at index %zd", Py_TYPE(item)->tp_name, i);
return NULL;
}
Py_DECREF(item);
}
// All items validated, now we can safely add them
for (Py_ssize_t i = 0; i < other_len; i++) {
PyObject* item = PySequence_GetItem(other, i);
if (!item) {
return NULL; // Shouldn't happen, but be safe
}
// Use the existing append method which handles grid references
PyObject* result = append(self, item);
Py_DECREF(item);
if (!result) {
return NULL; // append() failed
}
Py_DECREF(result); // append returns Py_None
}
Py_INCREF(self);
return (PyObject*)self;
}
int UIEntityCollection::setitem(PyUIEntityCollectionObject* self, Py_ssize_t index, PyObject* value) {

View file

@ -75,7 +75,7 @@ namespace mcrfpydef {
.tp_doc = "UIGridPoint object",
.tp_getset = UIGridPoint::getsetters,
//.tp_init = (initproc)PyUIGridPoint_init, // TODO Define the init function
.tp_new = PyType_GenericNew,
.tp_new = NULL, // Prevent instantiation from Python - Issue #12
};
static PyTypeObject PyUIGridPointStateType = {
@ -87,6 +87,6 @@ namespace mcrfpydef {
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = "UIGridPointState object", // TODO: Add PyUIGridPointState tp_init
.tp_getset = UIGridPointState::getsetters,
.tp_new = PyType_GenericNew,
.tp_new = NULL, // Prevent instantiation from Python - Issue #12
};
}

View file

@ -1,5 +1,6 @@
#include "UISprite.h"
#include "GameEngine.h"
#include "PyVector.h"
UIDrawable* UISprite::click_at(sf::Vector2f point)
{
@ -92,6 +93,10 @@ PyObject* UISprite::get_float_member(PyUISpriteObject* self, void* closure)
return PyFloat_FromDouble(self->data->getPosition().y);
else if (member_ptr == 2)
return PyFloat_FromDouble(self->data->getScale().x); // scale X and Y are identical, presently
else if (member_ptr == 3)
return PyFloat_FromDouble(self->data->getScale().x); // scale_x
else if (member_ptr == 4)
return PyFloat_FromDouble(self->data->getScale().y); // scale_y
else
{
PyErr_SetString(PyExc_AttributeError, "Invalid attribute");
@ -120,8 +125,12 @@ int UISprite::set_float_member(PyUISpriteObject* self, PyObject* value, void* cl
self->data->setPosition(sf::Vector2f(val, self->data->getPosition().y));
else if (member_ptr == 1) //y
self->data->setPosition(sf::Vector2f(self->data->getPosition().x, val));
else if (member_ptr == 2) // scale
else if (member_ptr == 2) // scale (uniform)
self->data->setScale(sf::Vector2f(val, val));
else if (member_ptr == 3) // scale_x
self->data->setScale(sf::Vector2f(val, self->data->getScale().y));
else if (member_ptr == 4) // scale_y
self->data->setScale(sf::Vector2f(self->data->getScale().x, val));
return 0;
}
@ -195,14 +204,40 @@ int UISprite::set_texture(PyUISpriteObject* self, PyObject* value, void* closure
return 0;
}
PyObject* UISprite::get_pos(PyUISpriteObject* self, void* closure)
{
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
auto obj = (PyVectorObject*)type->tp_alloc(type, 0);
if (obj) {
auto pos = self->data->getPosition();
obj->data = sf::Vector2f(pos.x, pos.y);
}
return (PyObject*)obj;
}
int UISprite::set_pos(PyUISpriteObject* self, PyObject* value, void* closure)
{
PyVectorObject* vec = PyVector::from_arg(value);
if (!vec) {
PyErr_SetString(PyExc_TypeError, "pos must be a Vector or convertible to Vector");
return -1;
}
self->data->setPosition(vec->data);
return 0;
}
PyGetSetDef UISprite::getsetters[] = {
{"x", (getter)UISprite::get_float_member, (setter)UISprite::set_float_member, "X coordinate of top-left corner", (void*)0},
{"y", (getter)UISprite::get_float_member, (setter)UISprite::set_float_member, "Y coordinate of top-left corner", (void*)1},
{"scale", (getter)UISprite::get_float_member, (setter)UISprite::set_float_member, "Size factor", (void*)2},
{"sprite_number", (getter)UISprite::get_int_member, (setter)UISprite::set_int_member, "Which sprite on the texture is shown", NULL},
{"scale", (getter)UISprite::get_float_member, (setter)UISprite::set_float_member, "Uniform size factor", (void*)2},
{"scale_x", (getter)UISprite::get_float_member, (setter)UISprite::set_float_member, "Horizontal scale factor", (void*)3},
{"scale_y", (getter)UISprite::get_float_member, (setter)UISprite::set_float_member, "Vertical scale factor", (void*)4},
{"sprite_index", (getter)UISprite::get_int_member, (setter)UISprite::set_int_member, "Which sprite on the texture is shown", NULL},
{"sprite_number", (getter)UISprite::get_int_member, (setter)UISprite::set_int_member, "Which sprite on the texture is shown (deprecated: use sprite_index)", NULL},
{"texture", (getter)UISprite::get_texture, (setter)UISprite::set_texture, "Texture object", NULL},
{"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UISPRITE},
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UISPRITE},
{"pos", (getter)UISprite::get_pos, (setter)UISprite::set_pos, "Position as a Vector", NULL},
{NULL}
};
@ -214,7 +249,7 @@ PyObject* UISprite::repr(PyUISpriteObject* self)
//auto sprite = self->data->sprite;
ss << "<Sprite (x=" << self->data->getPosition().x << ", y=" << self->data->getPosition().y << ", " <<
"scale=" << self->data->getScale().x << ", " <<
"sprite_number=" << self->data->getSpriteIndex() << ")>";
"sprite_index=" << self->data->getSpriteIndex() << ")>";
}
std::string repr_str = ss.str();
return PyUnicode_DecodeUTF8(repr_str.c_str(), repr_str.size(), "replace");
@ -228,10 +263,32 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
int sprite_index = 0;
PyObject* texture = NULL;
// First try to parse as (x, y, texture, ...)
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffOif",
const_cast<char**>(keywords), &x, &y, &texture, &sprite_index, &scale))
{
return -1;
PyErr_Clear(); // Clear the error
// Try to parse as ((x,y), texture, ...) or (Vector, texture, ...)
PyObject* pos_obj = nullptr;
const char* alt_keywords[] = { "pos", "texture", "sprite_index", "scale", nullptr };
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOif", const_cast<char**>(alt_keywords),
&pos_obj, &texture, &sprite_index, &scale))
{
return -1;
}
// Convert position argument to x, y
if (pos_obj) {
PyVectorObject* vec = PyVector::from_arg(pos_obj);
if (!vec) {
PyErr_SetString(PyExc_TypeError, "First argument must be a tuple (x, y) or Vector when not providing x, y separately");
return -1;
}
x = vec->data.x;
y = vec->data.y;
}
}
// Handle texture - allow None or use default
@ -288,7 +345,7 @@ bool UISprite::setProperty(const std::string& name, float value) {
}
bool UISprite::setProperty(const std::string& name, int value) {
if (name == "sprite_number") {
if (name == "sprite_index" || name == "sprite_number") {
setSpriteIndex(value);
return true;
}
@ -328,7 +385,7 @@ bool UISprite::getProperty(const std::string& name, float& value) const {
}
bool UISprite::getProperty(const std::string& name, int& value) const {
if (name == "sprite_number") {
if (name == "sprite_index" || name == "sprite_number") {
value = sprite_index;
return true;
}

View file

@ -55,6 +55,8 @@ public:
static int set_int_member(PyUISpriteObject* self, PyObject* value, void* closure);
static PyObject* get_texture(PyUISpriteObject* self, void* closure);
static int set_texture(PyUISpriteObject* self, PyObject* value, void* closure);
static PyObject* get_pos(PyUISpriteObject* self, void* closure);
static int set_pos(PyUISpriteObject* self, PyObject* value, void* closure);
static PyGetSetDef getsetters[];
static PyObject* repr(PyUISpriteObject* self);
static int init(PyUISpriteObject* self, PyObject* args, PyObject* kwds);