Squashed commit of the following: [interpreter_mode]
closes #63 closes #69 closes #59 closes #47 closes #2 closes #3 closes #33 closes #27 closes #73 closes #74 closes #78 I'd like to thank Claude Code for ~200-250M total tokens and 5-7M output tokens 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> commit9bd1561bfcAuthor: John McCardle <mccardle.john@gmail.com> Date: Sat Jul 5 11:20:07 2025 -0400 Alpha 0.1 release - Move RenderTexture (#6) out of alpha requirements, I don't need it that badly - alpha blockers resolved: * Animation system (#59) * Z-order rendering (#63) * Python Sequence Protocol (#69) * New README (#47) * Removed deprecated methods (#2, #3) 🍾 McRogueFace 0.1.0 commit43321487ebAuthor: John McCardle <mccardle.john@gmail.com> Date: Sat Jul 5 10:36:09 2025 -0400 Issue #63 (z-order rendering) complete - Archive z-order test files commit90c318104bAuthor: John McCardle <mccardle.john@gmail.com> Date: Sat Jul 5 10:34:06 2025 -0400 Fix Issue #63: Implement z-order rendering with dirty flag optimization - Add dirty flags to PyScene and UIFrame to track when sorting is needed - Implement lazy sorting - only sort when z_index changes or elements are added/removed - Make Frame children respect z_index (previously rendered in insertion order only) - Update UIDrawable::set_int to notify when z_index changes - Mark collections dirty on append, remove, setitem, and slice operations - Remove per-frame vector copy in PyScene::render for better performance commite4482e7189Author: John McCardle <mccardle.john@gmail.com> Date: Sat Jul 5 01:58:03 2025 -0400 Implement complete Python Sequence Protocol for collections (closes #69) Major implementation of the full sequence protocol for both UICollection and UIEntityCollection, making them behave like proper Python sequences. Core Features Implemented: - __setitem__ (collection[i] = value) with type validation - __delitem__ (del collection[i]) with proper cleanup - __contains__ (item in collection) by C++ pointer comparison - __add__ (collection + other) returns Python list - __iadd__ (collection += other) with full validation before modification - Negative indexing support throughout - Complete slice support (getting, setting, deletion) - Extended slices with step \!= 1 - index() and count() methods - Type safety enforced for all operations UICollection specifics: - Accepts Frame, Caption, Sprite, and Grid objects only - Preserves z_index when replacing items - Auto-assigns z_index on append (existing behavior maintained) UIEntityCollection specifics: - Accepts Entity objects only - Manages grid references on add/remove/replace - Uses std::list iteration with std::advance() Also includes: - Default value support for constructors: - Caption accepts None for font (uses default_font) - Grid accepts None for texture (uses default_texture) - Sprite accepts None for texture (uses default_texture) - Entity accepts None for texture (uses default_texture) This completes Issue #69, removing it as an Alpha Blocker. commit70cf44f8f0Author: John McCardle <mccardle.john@gmail.com> Date: Sat Jul 5 00:56:42 2025 -0400 Implement comprehensive animation system (closes #59) - Add Animation class with 30+ easing functions (linear, ease in/out, quad, cubic, elastic, bounce, etc.) - Add property system to all UI classes for animation support: - UIFrame: position, size, colors (including individual r/g/b/a components) - UICaption: position, size, text, colors - UISprite: position, scale, sprite_number (with sequence support) - UIGrid: position, size, camera center, zoom - UIEntity: position, sprite properties - Create AnimationManager singleton for frame-based updates - Add Python bindings through PyAnimation wrapper - Support for delta animations (relative values) - Fix segfault when running scripts directly (mcrf_module initialization) - Fix headless/windowed mode behavior to respect --headless flag - Animations run purely in C++ without Python callbacks per frame All UI properties are now animatable with smooth interpolation and professional easing curves. commit05bddae511Author: John McCardle <mccardle.john@gmail.com> Date: Fri Jul 4 06:59:02 2025 -0400 Update comprehensive documentation for Alpha release (Issue #47) - Completely rewrote README.md to reflect current features - Updated GitHub Pages documentation site with: - Modern landing page highlighting Crypt of Sokoban - Comprehensive API reference (2700+ lines) with exhaustive examples - Updated getting-started guide with installation and first game tutorial - 8 detailed tutorials covering all major game systems - Quick reference cheat sheet for common operations - Generated documentation screenshots showing UI elements - Fixed deprecated API references and added new features - Added automation API documentation - Included Python 3.12 requirement and platform-specific instructions Note: Text rendering in headless mode has limitations for screenshots commitaf6a5e090bAuthor: John McCardle <mccardle.john@gmail.com> Date: Thu Jul 3 21:43:58 2025 -0400 Update ROADMAP.md to reflect completion of Issues #2 and #3 - Marked both issues as completed with the removal of deprecated action system - Updated open issue count from ~50 to ~48 - These were both Alpha blockers, bringing us closer to release commit281800cd23Author: John McCardle <mccardle.john@gmail.com> Date: Thu Jul 3 21:43:22 2025 -0400 Remove deprecated registerPyAction/registerInputAction system (closes #2, closes #3) This is our largest net-negative commit yet\! Removed the entire deprecated action registration system that provided unnecessary two-step indirection: keyboard → action string → Python callback Removed components: - McRFPy_API::_registerPyAction() and _registerInputAction() methods - McRFPy_API::callbacks map for storing Python callables - McRFPy_API::doAction() method for executing callbacks - ACTIONPY macro from Scene.h for detecting "_py" suffixed actions - Scene::registerActionInjected() and unregisterActionInjected() methods - tests/api_registerPyAction_issue2_test.py (tested deprecated functionality) The game now exclusively uses keypressScene() for keyboard input handling, which is simpler and more direct. Also commented out the unused _camFollow function that referenced non-existent do_camfollow variable. commitcc8a7d20e8Author: John McCardle <mccardle.john@gmail.com> Date: Thu Jul 3 21:13:59 2025 -0400 Clean up temporary test files commitff83fd8bb1Author: John McCardle <mccardle.john@gmail.com> Date: Thu Jul 3 21:13:46 2025 -0400 Update ROADMAP.md to reflect massive progress today - Fixed 12+ critical bugs in a single session - Implemented 3 missing features (Entity.index, EntityCollection.extend, sprite validation) - Updated Phase 1 progress showing 11 of 12 items complete - Added detailed summary of today's achievements with issue numbers - Emphasized test-driven development approach used throughout commitdae400031fAuthor: John McCardle <mccardle.john@gmail.com> Date: Thu Jul 3 21:12:29 2025 -0400 Remove deprecated player_input and turn-based functions for Issue #3 Removed the commented-out player_input(), computerTurn(), and playerTurn() functions that were part of the old turn-based system. These are no longer needed as input is now handled through Scene callbacks. Partial fix for #3 commitcb0130b46eAuthor: John McCardle <mccardle.john@gmail.com> Date: Thu Jul 3 21:09:06 2025 -0400 Implement sprite index validation for Issue #33 Added validation to prevent setting sprite indices outside the valid range for a texture. The implementation: - Adds getSpriteCount() method to PyTexture to expose total sprites - Validates sprite_number setter to ensure index is within bounds - Provides clear error messages showing valid range - Works for both Sprite and Entity objects closes #33 commit1e7f5e9e7eAuthor: John McCardle <mccardle.john@gmail.com> Date: Thu Jul 3 21:05:47 2025 -0400 Implement EntityCollection.extend() method for Issue #27 Added extend() method to EntityCollection that accepts any iterable of Entity objects and adds them all to the collection. The method: - Accepts lists, tuples, generators, or any iterable - Validates all items are Entity objects - Sets the grid association for each added entity - Properly handles errors and empty iterables closes #27 commit923350137dAuthor: John McCardle <mccardle.john@gmail.com> Date: Thu Jul 3 21:02:14 2025 -0400 Implement Entity.index() method for Issue #73 Added index() method to Entity class that returns the entity's position in its parent grid's entity collection. This enables proper entity removal patterns using entity.index(). commit6134869371Author: John McCardle <mccardle.john@gmail.com> Date: Thu Jul 3 20:41:03 2025 -0400 Add validation to keypressScene() for non-callable arguments Added PyCallable_Check validation to ensure keypressScene() only accepts callable objects. Now properly raises TypeError with a clear error message when passed non-callable arguments like strings, numbers, None, or dicts. commit4715356b5eAuthor: John McCardle <mccardle.john@gmail.com> Date: Thu Jul 3 20:31:36 2025 -0400 Fix Sprite texture setter 'error return without exception set' Implemented the missing UISprite::set_texture method to properly: - Validate the input is a Texture instance - Update the sprite's texture using setTexture() - Return appropriate error messages for invalid inputs The setter now works correctly and no longer returns -1 without setting an exception. commit6dd1cec600Author: John McCardle <mccardle.john@gmail.com> Date: Thu Jul 3 20:27:32 2025 -0400 Fix Entity property setters and PyVector implementation Fixed the 'new style getargs format' error in Entity property setters by: - Implementing PyObject_to_sfVector2f/2i using PyVector::from_arg - Adding proper error checking in Entity::set_position - Implementing PyVector get_member/set_member for x/y properties - Fixing PyVector::from_arg to handle non-tuple arguments correctly Now Entity.pos and Entity.sprite_number setters work correctly with proper type validation. commitf82b861bcdAuthor: John McCardle <mccardle.john@gmail.com> Date: Thu Jul 3 19:48:33 2025 -0400 Fix Issue #74: Add missing Grid.grid_y property Added individual grid_x and grid_y getter properties to the Grid class to complement the existing grid_size property. This allows direct access to grid dimensions and fixes error messages that referenced these properties before they existed. closes #74 commit59e6f8d53dAuthor: John McCardle <mccardle.john@gmail.com> Date: Thu Jul 3 19:42:32 2025 -0400 Fix Issue #78: Middle mouse click no longer sends 'C' keyboard event The bug was caused by accessing event.key.code on a mouse event without checking the event type first. Since SFML uses a union for events, this read garbage data. The middle mouse button value (2) coincidentally matched the keyboard 'C' value (2), causing the spurious keyboard event. Fixed by adding event type check before accessing key-specific fields. Only keyboard events (KeyPressed/KeyReleased) now trigger key callbacks. Test added to verify middle clicks no longer generate keyboard events. Closes #78 commit1c71d8d4f7Author: John McCardle <mccardle.john@gmail.com> Date: Thu Jul 3 19:36:15 2025 -0400 Fix Grid to support None/null texture and fix error message bug - Allow Grid to be created with None as texture parameter - Use default cell dimensions (16x16) when no texture provided - Skip sprite rendering when texture is null, but still render colors - Fix issue #77: Corrected copy/paste error in Grid.at() error messages - Grid now functional for color-only rendering and entity positioning Test created to verify Grid works without texture, showing colored cells. Closes #77 commit18cfe93a44Author: John McCardle <mccardle.john@gmail.com> Date: Thu Jul 3 19:25:49 2025 -0400 Fix --exec interactive prompt bug and create comprehensive test suite Major fixes: - Fixed --exec entering Python REPL instead of game loop - Resolved screenshot transparency issue (requires timer callbacks) - Added debug output to trace Python initialization Test suite created: - 13 comprehensive tests covering all Python-exposed methods - Tests use timer callback pattern for proper game loop interaction - Discovered multiple critical bugs and missing features Critical bugs found: - Grid class segfaults on instantiation (blocks all Grid functionality) - Issue #78 confirmed: Middle mouse click sends 'C' keyboard event - Entity property setters have argument parsing errors - Sprite texture setter returns improper error - keypressScene() segfaults on non-callable arguments Documentation updates: - Updated CLAUDE.md with testing guidelines and TDD practices - Created test reports documenting all findings - Updated ROADMAP.md with test results and new priorities The Grid segfault is now the highest priority as it blocks all Grid-based functionality. commit9ad0b6850dAuthor: John McCardle <mccardle.john@gmail.com> Date: Thu Jul 3 15:55:24 2025 -0400 Update ROADMAP.md to reflect Python interpreter and automation API progress - Mark #32 (Python interpreter behavior) as 90% complete - All major Python flags implemented: -h, -V, -c, -m, -i - Script execution with proper sys.argv handling works - Only stdin (-) support missing - Note that new automation API enables: - Automated UI testing capabilities - Demo recording and playback - Accessibility testing support - Flag issues #53 and #45 as potentially aided by automation API commit7ec4698653Author: John McCardle <mccardle.john@gmail.com> Date: Thu Jul 3 14:57:59 2025 -0400 Update ROADMAP.md to remove closed issues - Remove #72 (iterator improvements - closed) - Remove #51 (UIEntity derive from UIDrawable - closed) - Update issue counts: 64 open issues from original 78 - Update dependencies and references to reflect closed issues - Clarify that core iterators are complete, only grid points remain commit68c1a016b0Author: John McCardle <mccardle.john@gmail.com> Date: Thu Jul 3 14:27:01 2025 -0400 Implement --exec flag and PyAutoGUI-compatible automation API - Add --exec flag to execute multiple scripts before main program - Scripts are executed in order and share Python interpreter state - Implement full PyAutoGUI-compatible automation API in McRFPy_Automation - Add screenshot, mouse control, keyboard input capabilities - Fix Python initialization issues when multiple scripts are loaded - Update CommandLineParser to handle --exec with proper sys.argv management - Add comprehensive examples and documentation This enables automation testing by allowing test scripts to run alongside games using the same Python environment. The automation API provides event injection into the SFML render loop for UI testing. Closes #32 partially (Python interpreter emulation) References automation testing requirements commit763fa201f0Author: John McCardle <mccardle.john@gmail.com> Date: Thu Jul 3 10:43:17 2025 -0400 Python command emulation commita44b8c93e9Author: John McCardle <mccardle.john@gmail.com> Date: Thu Jul 3 09:42:46 2025 -0400 Prep: Cleanup for interpreter mode
This commit is contained in:
parent
167636ce8c
commit
d03182d347
81 changed files with 8608 additions and 400 deletions
|
|
@ -6,6 +6,8 @@
|
|||
#include "UIGrid.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "PyObjectUtils.h"
|
||||
#include <climits>
|
||||
#include <algorithm>
|
||||
|
||||
using namespace mcrfpydef;
|
||||
|
||||
|
|
@ -148,15 +150,394 @@ PyObject* UICollection::getitem(PyUICollectionObject* self, Py_ssize_t index) {
|
|||
|
||||
}
|
||||
|
||||
int UICollection::setitem(PyUICollectionObject* self, Py_ssize_t index, PyObject* value) {
|
||||
auto vec = self->data.get();
|
||||
if (!vec) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Handle negative indexing
|
||||
while (index < 0) index += self->data->size();
|
||||
|
||||
// Bounds check
|
||||
if (index >= self->data->size()) {
|
||||
PyErr_SetString(PyExc_IndexError, "UICollection assignment index out of range");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Handle deletion
|
||||
if (value == NULL) {
|
||||
self->data->erase(self->data->begin() + index);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Type checking - must be a UIDrawable subclass
|
||||
if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")) &&
|
||||
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")) &&
|
||||
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")) &&
|
||||
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
|
||||
PyErr_SetString(PyExc_TypeError, "UICollection can only contain Frame, Caption, Sprite, and Grid objects");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Get the C++ object from the Python object
|
||||
std::shared_ptr<UIDrawable> new_drawable = nullptr;
|
||||
int old_z_index = (*vec)[index]->z_index; // Preserve the z_index
|
||||
|
||||
if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) {
|
||||
PyUIFrameObject* frame = (PyUIFrameObject*)value;
|
||||
new_drawable = frame->data;
|
||||
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) {
|
||||
PyUICaptionObject* caption = (PyUICaptionObject*)value;
|
||||
new_drawable = caption->data;
|
||||
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) {
|
||||
PyUISpriteObject* sprite = (PyUISpriteObject*)value;
|
||||
new_drawable = sprite->data;
|
||||
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
|
||||
PyUIGridObject* grid = (PyUIGridObject*)value;
|
||||
new_drawable = grid->data;
|
||||
}
|
||||
|
||||
if (!new_drawable) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Failed to extract C++ object from Python object");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Preserve the z_index of the replaced element
|
||||
new_drawable->z_index = old_z_index;
|
||||
|
||||
// Replace the element
|
||||
(*vec)[index] = new_drawable;
|
||||
|
||||
// Mark scene as needing resort after replacing element
|
||||
McRFPy_API::markSceneNeedsSort();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int UICollection::contains(PyUICollectionObject* self, PyObject* value) {
|
||||
auto vec = self->data.get();
|
||||
if (!vec) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Type checking - must be a UIDrawable subclass
|
||||
if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")) &&
|
||||
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")) &&
|
||||
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")) &&
|
||||
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
|
||||
// Not a valid type, so it can't be in the collection
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Get the C++ object from the Python object
|
||||
std::shared_ptr<UIDrawable> search_drawable = nullptr;
|
||||
|
||||
if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) {
|
||||
PyUIFrameObject* frame = (PyUIFrameObject*)value;
|
||||
search_drawable = frame->data;
|
||||
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) {
|
||||
PyUICaptionObject* caption = (PyUICaptionObject*)value;
|
||||
search_drawable = caption->data;
|
||||
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) {
|
||||
PyUISpriteObject* sprite = (PyUISpriteObject*)value;
|
||||
search_drawable = sprite->data;
|
||||
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
|
||||
PyUIGridObject* grid = (PyUIGridObject*)value;
|
||||
search_drawable = grid->data;
|
||||
}
|
||||
|
||||
if (!search_drawable) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Search for the object by comparing C++ pointers
|
||||
for (const auto& drawable : *vec) {
|
||||
if (drawable.get() == search_drawable.get()) {
|
||||
return 1; // Found
|
||||
}
|
||||
}
|
||||
|
||||
return 0; // Not found
|
||||
}
|
||||
|
||||
PyObject* UICollection::concat(PyUICollectionObject* 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 UICollection");
|
||||
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
|
||||
for (Py_ssize_t i = 0; i < self_len; i++) {
|
||||
PyObject* item = convertDrawableToPython((*self->data)[i]);
|
||||
if (!item) {
|
||||
Py_DECREF(result_list);
|
||||
return NULL;
|
||||
}
|
||||
PyList_SET_ITEM(result_list, i, item); // Steals reference
|
||||
}
|
||||
|
||||
// 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* UICollection::inplace_concat(PyUICollectionObject* self, PyObject* other) {
|
||||
if (!PySequence_Check(other)) {
|
||||
PyErr_SetString(PyExc_TypeError, "can only concatenate sequence to UICollection");
|
||||
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, "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);
|
||||
PyErr_Format(PyExc_TypeError,
|
||||
"UICollection can only contain Frame, Caption, Sprite, and Grid 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 z_index assignment
|
||||
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;
|
||||
}
|
||||
|
||||
PyObject* UICollection::subscript(PyUICollectionObject* self, PyObject* key) {
|
||||
if (PyLong_Check(key)) {
|
||||
// Single index - delegate to sq_item
|
||||
Py_ssize_t index = PyLong_AsSsize_t(key);
|
||||
if (index == -1 && PyErr_Occurred()) {
|
||||
return NULL;
|
||||
}
|
||||
return getitem(self, index);
|
||||
} else if (PySlice_Check(key)) {
|
||||
// Handle slice
|
||||
Py_ssize_t start, stop, step, slicelength;
|
||||
|
||||
if (PySlice_GetIndicesEx(key, self->data->size(), &start, &stop, &step, &slicelength) < 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
PyObject* result_list = PyList_New(slicelength);
|
||||
if (!result_list) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (Py_ssize_t i = 0, cur = start; i < slicelength; i++, cur += step) {
|
||||
PyObject* item = convertDrawableToPython((*self->data)[cur]);
|
||||
if (!item) {
|
||||
Py_DECREF(result_list);
|
||||
return NULL;
|
||||
}
|
||||
PyList_SET_ITEM(result_list, i, item); // Steals reference
|
||||
}
|
||||
|
||||
return result_list;
|
||||
} else {
|
||||
PyErr_Format(PyExc_TypeError, "UICollection indices must be integers or slices, not %.200s",
|
||||
Py_TYPE(key)->tp_name);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
int UICollection::ass_subscript(PyUICollectionObject* self, PyObject* key, PyObject* value) {
|
||||
if (PyLong_Check(key)) {
|
||||
// Single index - delegate to sq_ass_item
|
||||
Py_ssize_t index = PyLong_AsSsize_t(key);
|
||||
if (index == -1 && PyErr_Occurred()) {
|
||||
return -1;
|
||||
}
|
||||
return setitem(self, index, value);
|
||||
} else if (PySlice_Check(key)) {
|
||||
// Handle slice assignment/deletion
|
||||
Py_ssize_t start, stop, step, slicelength;
|
||||
|
||||
if (PySlice_GetIndicesEx(key, self->data->size(), &start, &stop, &step, &slicelength) < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (value == NULL) {
|
||||
// Deletion
|
||||
if (step != 1) {
|
||||
// For non-contiguous slices, delete from highest to lowest to maintain indices
|
||||
std::vector<Py_ssize_t> indices;
|
||||
for (Py_ssize_t i = 0, cur = start; i < slicelength; i++, cur += step) {
|
||||
indices.push_back(cur);
|
||||
}
|
||||
// Sort in descending order and delete
|
||||
std::sort(indices.begin(), indices.end(), std::greater<Py_ssize_t>());
|
||||
for (Py_ssize_t idx : indices) {
|
||||
self->data->erase(self->data->begin() + idx);
|
||||
}
|
||||
} else {
|
||||
// Contiguous slice - can delete in one go
|
||||
self->data->erase(self->data->begin() + start, self->data->begin() + stop);
|
||||
}
|
||||
|
||||
// Mark scene as needing resort after slice deletion
|
||||
McRFPy_API::markSceneNeedsSort();
|
||||
|
||||
return 0;
|
||||
} else {
|
||||
// Assignment
|
||||
if (!PySequence_Check(value)) {
|
||||
PyErr_SetString(PyExc_TypeError, "can only assign sequence to slice");
|
||||
return -1;
|
||||
}
|
||||
|
||||
Py_ssize_t value_len = PySequence_Length(value);
|
||||
if (value_len == -1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Validate all items first
|
||||
std::vector<std::shared_ptr<UIDrawable>> new_items;
|
||||
for (Py_ssize_t i = 0; i < value_len; i++) {
|
||||
PyObject* item = PySequence_GetItem(value, i);
|
||||
if (!item) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Type check and extract C++ object
|
||||
std::shared_ptr<UIDrawable> drawable = nullptr;
|
||||
|
||||
if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) {
|
||||
drawable = ((PyUIFrameObject*)item)->data;
|
||||
} else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) {
|
||||
drawable = ((PyUICaptionObject*)item)->data;
|
||||
} else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) {
|
||||
drawable = ((PyUISpriteObject*)item)->data;
|
||||
} else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
|
||||
drawable = ((PyUIGridObject*)item)->data;
|
||||
} else {
|
||||
Py_DECREF(item);
|
||||
PyErr_Format(PyExc_TypeError,
|
||||
"UICollection can only contain Frame, Caption, Sprite, and Grid objects; "
|
||||
"got %s at index %zd", Py_TYPE(item)->tp_name, i);
|
||||
return -1;
|
||||
}
|
||||
|
||||
Py_DECREF(item);
|
||||
new_items.push_back(drawable);
|
||||
}
|
||||
|
||||
// Now perform the assignment
|
||||
if (step == 1) {
|
||||
// Contiguous slice
|
||||
if (slicelength != value_len) {
|
||||
// Need to resize
|
||||
auto it_start = self->data->begin() + start;
|
||||
auto it_stop = self->data->begin() + stop;
|
||||
self->data->erase(it_start, it_stop);
|
||||
self->data->insert(self->data->begin() + start, new_items.begin(), new_items.end());
|
||||
} else {
|
||||
// Same size, just replace
|
||||
for (Py_ssize_t i = 0; i < slicelength; i++) {
|
||||
// Preserve z_index
|
||||
new_items[i]->z_index = (*self->data)[start + i]->z_index;
|
||||
(*self->data)[start + i] = new_items[i];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Extended slice
|
||||
if (slicelength != value_len) {
|
||||
PyErr_Format(PyExc_ValueError,
|
||||
"attempt to assign sequence of size %zd to extended slice of size %zd",
|
||||
value_len, slicelength);
|
||||
return -1;
|
||||
}
|
||||
for (Py_ssize_t i = 0, cur = start; i < slicelength; i++, cur += step) {
|
||||
// Preserve z_index
|
||||
new_items[i]->z_index = (*self->data)[cur]->z_index;
|
||||
(*self->data)[cur] = new_items[i];
|
||||
}
|
||||
}
|
||||
|
||||
// Mark scene as needing resort after slice assignment
|
||||
McRFPy_API::markSceneNeedsSort();
|
||||
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
PyErr_Format(PyExc_TypeError, "UICollection indices must be integers or slices, not %.200s",
|
||||
Py_TYPE(key)->tp_name);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
PyMappingMethods UICollection::mpmethods = {
|
||||
.mp_length = (lenfunc)UICollection::len,
|
||||
.mp_subscript = (binaryfunc)UICollection::subscript,
|
||||
.mp_ass_subscript = (objobjargproc)UICollection::ass_subscript
|
||||
};
|
||||
|
||||
PySequenceMethods UICollection::sqmethods = {
|
||||
.sq_length = (lenfunc)UICollection::len,
|
||||
.sq_concat = (binaryfunc)UICollection::concat,
|
||||
.sq_repeat = NULL,
|
||||
.sq_item = (ssizeargfunc)UICollection::getitem,
|
||||
//.sq_item_by_index = PyUICollection_getitem
|
||||
//.sq_slice - return a subset of the iterable
|
||||
//.sq_ass_item - called when `o[x] = y` is executed (x is any object type)
|
||||
//.sq_ass_slice - cool; no thanks, for now
|
||||
//.sq_contains - called when `x in o` is executed
|
||||
//.sq_ass_item_by_index - called when `o[x] = y` is executed (x is explictly an integer)
|
||||
.was_sq_slice = NULL,
|
||||
.sq_ass_item = (ssizeobjargproc)UICollection::setitem,
|
||||
.was_sq_ass_slice = NULL,
|
||||
.sq_contains = (objobjproc)UICollection::contains,
|
||||
.sq_inplace_concat = (binaryfunc)UICollection::inplace_concat,
|
||||
.sq_inplace_repeat = NULL
|
||||
};
|
||||
|
||||
/* Idiomatic way to fetch complete types from the API rather than referencing their PyTypeObject struct
|
||||
|
|
@ -173,6 +554,12 @@ PyObject* UICollection::append(PyUICollectionObject* self, PyObject* o)
|
|||
// if not UIDrawable subclass, reject it
|
||||
// self->data->push_back( c++ object inside o );
|
||||
|
||||
// Ensure module is initialized
|
||||
if (!McRFPy_API::mcrf_module) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "mcrfpy module not initialized");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// this would be a great use case for .tp_base
|
||||
if (!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")) &&
|
||||
!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")) &&
|
||||
|
|
@ -184,26 +571,45 @@ PyObject* UICollection::append(PyUICollectionObject* self, PyObject* o)
|
|||
return NULL;
|
||||
}
|
||||
|
||||
// Calculate z_index for the new element
|
||||
int new_z_index = 0;
|
||||
if (!self->data->empty()) {
|
||||
// Get the z_index of the last element and add 10
|
||||
int last_z = self->data->back()->z_index;
|
||||
if (last_z <= INT_MAX - 10) {
|
||||
new_z_index = last_z + 10;
|
||||
} else {
|
||||
new_z_index = INT_MAX;
|
||||
}
|
||||
}
|
||||
|
||||
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")))
|
||||
{
|
||||
PyUIFrameObject* frame = (PyUIFrameObject*)o;
|
||||
frame->data->z_index = new_z_index;
|
||||
self->data->push_back(frame->data);
|
||||
}
|
||||
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")))
|
||||
{
|
||||
PyUICaptionObject* caption = (PyUICaptionObject*)o;
|
||||
caption->data->z_index = new_z_index;
|
||||
self->data->push_back(caption->data);
|
||||
}
|
||||
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")))
|
||||
{
|
||||
PyUISpriteObject* sprite = (PyUISpriteObject*)o;
|
||||
sprite->data->z_index = new_z_index;
|
||||
self->data->push_back(sprite->data);
|
||||
}
|
||||
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid")))
|
||||
{
|
||||
PyUIGridObject* grid = (PyUIGridObject*)o;
|
||||
grid->data->z_index = new_z_index;
|
||||
self->data->push_back(grid->data);
|
||||
}
|
||||
|
||||
// Mark scene as needing resort after adding element
|
||||
McRFPy_API::markSceneNeedsSort();
|
||||
|
||||
Py_INCREF(Py_None);
|
||||
return Py_None;
|
||||
|
|
@ -217,27 +623,121 @@ PyObject* UICollection::remove(PyUICollectionObject* self, PyObject* o)
|
|||
return NULL;
|
||||
}
|
||||
long index = PyLong_AsLong(o);
|
||||
|
||||
// Handle negative indexing
|
||||
while (index < 0) index += self->data->size();
|
||||
|
||||
if (index >= self->data->size())
|
||||
{
|
||||
PyErr_SetString(PyExc_ValueError, "Index out of range");
|
||||
return NULL;
|
||||
}
|
||||
else if (index < 0)
|
||||
{
|
||||
PyErr_SetString(PyExc_NotImplementedError, "reverse indexing is not implemented.");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// release the shared pointer at self->data[index];
|
||||
self->data->erase(self->data->begin() + index);
|
||||
|
||||
// Mark scene as needing resort after removing element
|
||||
McRFPy_API::markSceneNeedsSort();
|
||||
|
||||
Py_INCREF(Py_None);
|
||||
return Py_None;
|
||||
}
|
||||
|
||||
PyObject* UICollection::index_method(PyUICollectionObject* self, PyObject* value) {
|
||||
auto vec = self->data.get();
|
||||
if (!vec) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Type checking - must be a UIDrawable subclass
|
||||
if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")) &&
|
||||
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")) &&
|
||||
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")) &&
|
||||
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
|
||||
PyErr_SetString(PyExc_TypeError, "UICollection.index requires a Frame, Caption, Sprite, or Grid object");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Get the C++ object from the Python object
|
||||
std::shared_ptr<UIDrawable> search_drawable = nullptr;
|
||||
|
||||
if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) {
|
||||
search_drawable = ((PyUIFrameObject*)value)->data;
|
||||
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) {
|
||||
search_drawable = ((PyUICaptionObject*)value)->data;
|
||||
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) {
|
||||
search_drawable = ((PyUISpriteObject*)value)->data;
|
||||
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
|
||||
search_drawable = ((PyUIGridObject*)value)->data;
|
||||
}
|
||||
|
||||
if (!search_drawable) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Failed to extract C++ object from Python object");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Search for the object
|
||||
for (size_t i = 0; i < vec->size(); i++) {
|
||||
if ((*vec)[i].get() == search_drawable.get()) {
|
||||
return PyLong_FromSsize_t(i);
|
||||
}
|
||||
}
|
||||
|
||||
PyErr_SetString(PyExc_ValueError, "value not in UICollection");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
PyObject* UICollection::count(PyUICollectionObject* self, PyObject* value) {
|
||||
auto vec = self->data.get();
|
||||
if (!vec) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Type checking - must be a UIDrawable subclass
|
||||
if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")) &&
|
||||
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")) &&
|
||||
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")) &&
|
||||
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
|
||||
// Not a valid type, so count is 0
|
||||
return PyLong_FromLong(0);
|
||||
}
|
||||
|
||||
// Get the C++ object from the Python object
|
||||
std::shared_ptr<UIDrawable> search_drawable = nullptr;
|
||||
|
||||
if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) {
|
||||
search_drawable = ((PyUIFrameObject*)value)->data;
|
||||
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) {
|
||||
search_drawable = ((PyUICaptionObject*)value)->data;
|
||||
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) {
|
||||
search_drawable = ((PyUISpriteObject*)value)->data;
|
||||
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
|
||||
search_drawable = ((PyUIGridObject*)value)->data;
|
||||
}
|
||||
|
||||
if (!search_drawable) {
|
||||
return PyLong_FromLong(0);
|
||||
}
|
||||
|
||||
// Count occurrences
|
||||
Py_ssize_t count = 0;
|
||||
for (const auto& drawable : *vec) {
|
||||
if (drawable.get() == search_drawable.get()) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return PyLong_FromSsize_t(count);
|
||||
}
|
||||
|
||||
PyMethodDef UICollection::methods[] = {
|
||||
{"append", (PyCFunction)UICollection::append, METH_O},
|
||||
//{"extend", (PyCFunction)PyUICollection_extend, METH_O}, // TODO
|
||||
{"remove", (PyCFunction)UICollection::remove, METH_O},
|
||||
{"index", (PyCFunction)UICollection::index_method, METH_O},
|
||||
{"count", (PyCFunction)UICollection::count, METH_O},
|
||||
{NULL, NULL, 0, NULL}
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue