From d6ef29f3cd6aa8b245026689a46cefac638a49e3 Mon Sep 17 00:00:00 2001 From: John McCardle Date: Sat, 10 Jan 2026 08:37:31 -0500 Subject: [PATCH] Grid code quality improvements * Grid [x, y] subscript - convenience for `.at()` * Extract UIEntityCollection - cleanup of UIGrid.cpp * Thread-safe type cache - PyTypeCache * Exception-safe extend() - validate before modify --- src/McRFPy_API.cpp | 13 +- src/PyTypeCache.cpp | 135 +++++ src/PyTypeCache.h | 64 +++ src/UIEntityCollection.cpp | 1078 ++++++++++++++++++++++++++++++++++++ src/UIEntityCollection.h | 145 +++++ src/UIGrid.cpp | 1040 ++-------------------------------- src/UIGrid.h | 114 +--- 7 files changed, 1492 insertions(+), 1097 deletions(-) create mode 100644 src/PyTypeCache.cpp create mode 100644 src/PyTypeCache.h create mode 100644 src/UIEntityCollection.cpp create mode 100644 src/UIEntityCollection.h diff --git a/src/McRFPy_API.cpp b/src/McRFPy_API.cpp index d9ea65f..5bf0400 100644 --- a/src/McRFPy_API.cpp +++ b/src/McRFPy_API.cpp @@ -3,6 +3,7 @@ #include "McRFPy_Automation.h" #include "McRFPy_Libtcod.h" #include "McRFPy_Doc.h" +#include "PyTypeCache.h" // Thread-safe cached Python types #include "platform.h" #include "PyAnimation.h" #include "PyDrawable.h" @@ -549,12 +550,20 @@ PyObject* PyInit_mcrfpy() PyObject* libtcod_module = McRFPy_Libtcod::init_libtcod_module(); if (libtcod_module != NULL) { PyModule_AddObject(m, "libtcod", libtcod_module); - + // Also add to sys.modules for proper import behavior PyObject* sys_modules = PyImport_GetModuleDict(); PyDict_SetItemString(sys_modules, "mcrfpy.libtcod", libtcod_module); } - + + // Initialize PyTypeCache for thread-safe type lookups + // This must be done after all types are added to the module + if (!PyTypeCache::initialize(m)) { + // Failed to initialize type cache - this is a critical error + // Error message already set by PyTypeCache::initialize + return NULL; + } + //McRFPy_API::mcrf_module = m; return m; } diff --git a/src/PyTypeCache.cpp b/src/PyTypeCache.cpp new file mode 100644 index 0000000..22d18a9 --- /dev/null +++ b/src/PyTypeCache.cpp @@ -0,0 +1,135 @@ +// PyTypeCache.cpp - Thread-safe Python type caching implementation +#include "PyTypeCache.h" + +// Static member definitions +std::atomic PyTypeCache::entity_type{nullptr}; +std::atomic PyTypeCache::grid_type{nullptr}; +std::atomic PyTypeCache::frame_type{nullptr}; +std::atomic PyTypeCache::caption_type{nullptr}; +std::atomic PyTypeCache::sprite_type{nullptr}; +std::atomic PyTypeCache::texture_type{nullptr}; +std::atomic PyTypeCache::color_type{nullptr}; +std::atomic PyTypeCache::vector_type{nullptr}; +std::atomic PyTypeCache::font_type{nullptr}; +std::atomic PyTypeCache::initialized{false}; +std::mutex PyTypeCache::init_mutex; + +PyTypeObject* PyTypeCache::cacheType(PyObject* module, const char* name, + std::atomic& cache) { + PyObject* type_obj = PyObject_GetAttrString(module, name); + if (!type_obj) { + PyErr_Format(PyExc_RuntimeError, + "PyTypeCache: Failed to get type '%s' from module", name); + return nullptr; + } + + if (!PyType_Check(type_obj)) { + Py_DECREF(type_obj); + PyErr_Format(PyExc_TypeError, + "PyTypeCache: '%s' is not a type object", name); + return nullptr; + } + + // Store in cache - we keep the reference permanently + // Using memory_order_release ensures the pointer is visible to other threads + // after they see initialized=true + cache.store((PyTypeObject*)type_obj, std::memory_order_release); + + return (PyTypeObject*)type_obj; +} + +bool PyTypeCache::initialize(PyObject* module) { + std::lock_guard lock(init_mutex); + + // Double-check pattern - might have been initialized while waiting for lock + if (initialized.load(std::memory_order_acquire)) { + return true; + } + + // Cache all types + if (!cacheType(module, "Entity", entity_type)) return false; + if (!cacheType(module, "Grid", grid_type)) return false; + if (!cacheType(module, "Frame", frame_type)) return false; + if (!cacheType(module, "Caption", caption_type)) return false; + if (!cacheType(module, "Sprite", sprite_type)) return false; + if (!cacheType(module, "Texture", texture_type)) return false; + if (!cacheType(module, "Color", color_type)) return false; + if (!cacheType(module, "Vector", vector_type)) return false; + if (!cacheType(module, "Font", font_type)) return false; + + // Mark as initialized - release ensures all stores above are visible + initialized.store(true, std::memory_order_release); + + return true; +} + +void PyTypeCache::finalize() { + std::lock_guard lock(init_mutex); + + if (!initialized.load(std::memory_order_acquire)) { + return; + } + + // Release all cached references + auto release = [](std::atomic& cache) { + PyTypeObject* type = cache.exchange(nullptr, std::memory_order_acq_rel); + if (type) { + Py_DECREF(type); + } + }; + + release(entity_type); + release(grid_type); + release(frame_type); + release(caption_type); + release(sprite_type); + release(texture_type); + release(color_type); + release(vector_type); + release(font_type); + + initialized.store(false, std::memory_order_release); +} + +bool PyTypeCache::isInitialized() { + return initialized.load(std::memory_order_acquire); +} + +// Type accessors - lock-free reads after initialization +// Using memory_order_acquire ensures we see the pointer stored during init + +PyTypeObject* PyTypeCache::Entity() { + return entity_type.load(std::memory_order_acquire); +} + +PyTypeObject* PyTypeCache::Grid() { + return grid_type.load(std::memory_order_acquire); +} + +PyTypeObject* PyTypeCache::Frame() { + return frame_type.load(std::memory_order_acquire); +} + +PyTypeObject* PyTypeCache::Caption() { + return caption_type.load(std::memory_order_acquire); +} + +PyTypeObject* PyTypeCache::Sprite() { + return sprite_type.load(std::memory_order_acquire); +} + +PyTypeObject* PyTypeCache::Texture() { + return texture_type.load(std::memory_order_acquire); +} + +PyTypeObject* PyTypeCache::Color() { + return color_type.load(std::memory_order_acquire); +} + +PyTypeObject* PyTypeCache::Vector() { + return vector_type.load(std::memory_order_acquire); +} + +PyTypeObject* PyTypeCache::Font() { + return font_type.load(std::memory_order_acquire); +} diff --git a/src/PyTypeCache.h b/src/PyTypeCache.h new file mode 100644 index 0000000..633cb54 --- /dev/null +++ b/src/PyTypeCache.h @@ -0,0 +1,64 @@ +#pragma once +// PyTypeCache.h - Thread-safe caching of Python type objects +// +// This module provides a centralized, thread-safe way to cache Python type +// references. It eliminates the refcount leaks from repeated +// PyObject_GetAttrString calls and is compatible with free-threading (PEP 703). +// +// Usage: +// PyTypeObject* entity_type = PyTypeCache::Entity(); +// if (!entity_type) return NULL; // Error already set +// +// The cache is populated during module initialization and the types are +// held for the lifetime of the interpreter. + +#include "Python.h" +#include +#include + +class PyTypeCache { +public: + // Initialize the cache - call once during module init after types are ready + // Returns false and sets Python error on failure + static bool initialize(PyObject* module); + + // Finalize - release references (call during module cleanup if needed) + static void finalize(); + + // Type accessors - return borrowed references (no DECREF needed) + // These are thread-safe and lock-free after initialization + static PyTypeObject* Entity(); + static PyTypeObject* Grid(); + static PyTypeObject* Frame(); + static PyTypeObject* Caption(); + static PyTypeObject* Sprite(); + static PyTypeObject* Texture(); + static PyTypeObject* Color(); + static PyTypeObject* Vector(); + static PyTypeObject* Font(); + + // Check if initialized + static bool isInitialized(); + +private: + // Cached type pointers - atomic for thread-safe reads + static std::atomic entity_type; + static std::atomic grid_type; + static std::atomic frame_type; + static std::atomic caption_type; + static std::atomic sprite_type; + static std::atomic texture_type; + static std::atomic color_type; + static std::atomic vector_type; + static std::atomic font_type; + + // Initialization flag + static std::atomic initialized; + + // Mutex for initialization (only used during init, not for reads) + static std::mutex init_mutex; + + // Helper to fetch and cache a type + static PyTypeObject* cacheType(PyObject* module, const char* name, + std::atomic& cache); +}; diff --git a/src/UIEntityCollection.cpp b/src/UIEntityCollection.cpp new file mode 100644 index 0000000..cd11181 --- /dev/null +++ b/src/UIEntityCollection.cpp @@ -0,0 +1,1078 @@ +// UIEntityCollection.cpp - Implementation of EntityCollection Python type +// +// Extracted from UIGrid.cpp as part of code organization cleanup. +// This file contains all EntityCollection and UIEntityCollectionIter methods. + +#include "UIEntityCollection.h" +#include "UIEntity.h" +#include "UIGrid.h" +#include "McRFPy_API.h" +#include "PyTypeCache.h" +#include "PythonObjectCache.h" +#include +#include + +// ============================================================================ +// UIEntityCollectionIter implementation +// ============================================================================ + +int UIEntityCollectionIter::init(PyUIEntityCollectionIterObject* self, PyObject* args, PyObject* kwds) +{ + PyErr_SetString(PyExc_TypeError, "UIEntityCollectionIter cannot be instantiated directly"); + return -1; +} + +PyObject* UIEntityCollectionIter::next(PyUIEntityCollectionIterObject* self) +{ + // Check for collection modification during iteration + if (static_cast(self->data->size()) != self->start_size) + { + PyErr_SetString(PyExc_RuntimeError, "EntityCollection changed size during iteration"); + return NULL; + } + + // Check if we've reached the end + if (self->current == self->end) + { + PyErr_SetNone(PyExc_StopIteration); + return NULL; + } + + // Get current element and advance iterator - O(1) operation + auto target = *self->current; + ++self->current; + + // 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 + PyTypeObject* entity_type = PyTypeCache::Entity(); + if (!entity_type) { + PyErr_SetString(PyExc_RuntimeError, "Entity type not initialized in cache"); + return NULL; + } + + auto o = (PyUIEntityObject*)entity_type->tp_alloc(entity_type, 0); + if (!o) return NULL; + + o->data = std::static_pointer_cast(target); + o->weakreflist = NULL; + return (PyObject*)o; +} + +PyObject* UIEntityCollectionIter::repr(PyUIEntityCollectionIterObject* self) +{ + std::ostringstream ss; + if (!self->data) { + ss << ""; + } else { + auto remaining = std::distance(self->current, self->end); + auto total = self->data->size(); + ss << ""; + } + std::string repr_str = ss.str(); + return PyUnicode_DecodeUTF8(repr_str.c_str(), repr_str.size(), "replace"); +} + +// ============================================================================ +// UIEntityCollection - Sequence protocol +// ============================================================================ + +Py_ssize_t UIEntityCollection::len(PyUIEntityCollectionObject* self) { + return self->data ? self->data->size() : 0; +} + +PyObject* UIEntityCollection::getitem(PyUIEntityCollectionObject* self, Py_ssize_t index) { + auto vec = self->data.get(); + if (!vec) { + PyErr_SetString(PyExc_RuntimeError, "EntityCollection data is null"); + return NULL; + } + + // Handle negative indexing + Py_ssize_t size = static_cast(vec->size()); + if (index < 0) index += size; + if (index < 0 || index >= size) { + PyErr_SetString(PyExc_IndexError, "EntityCollection index out of range"); + return NULL; + } + + auto it = vec->begin(); + std::advance(it, index); + auto target = *it; + + // Check cache first to preserve derived class + if (target->serial_number != 0) { + PyObject* cached = PythonObjectCache::getInstance().lookup(target->serial_number); + if (cached) { + return cached; // Already INCREF'd by lookup + } + } + + // Legacy: If the entity has a stored Python object reference, return that + if (target->self != nullptr) { + Py_INCREF(target->self); + return target->self; + } + + // Otherwise, create a new base Entity object + PyTypeObject* entity_type = PyTypeCache::Entity(); + if (!entity_type) { + PyErr_SetString(PyExc_RuntimeError, "Entity type not initialized in cache"); + return NULL; + } + + auto o = (PyUIEntityObject*)entity_type->tp_alloc(entity_type, 0); + if (!o) return NULL; + + o->data = std::static_pointer_cast(target); + o->weakreflist = NULL; + return (PyObject*)o; +} + +int UIEntityCollection::setitem(PyUIEntityCollectionObject* self, Py_ssize_t index, PyObject* value) { + auto list = self->data.get(); + if (!list) { + PyErr_SetString(PyExc_RuntimeError, "EntityCollection data is null"); + return -1; + } + + // Handle negative indexing + Py_ssize_t size = static_cast(list->size()); + if (index < 0) index += size; + if (index < 0 || index >= size) { + PyErr_SetString(PyExc_IndexError, "EntityCollection assignment index out of range"); + return -1; + } + + auto it = list->begin(); + std::advance(it, index); + + // Handle deletion + if (value == NULL) { + // Remove from spatial hash before removing from list + if (self->grid) { + self->grid->spatial_hash.remove(*it); + } + (*it)->grid = nullptr; + list->erase(it); + return 0; + } + + // Type checking using cached type + PyTypeObject* entity_type = PyTypeCache::Entity(); + if (!entity_type) { + PyErr_SetString(PyExc_RuntimeError, "Entity type not initialized in cache"); + return -1; + } + + if (!PyObject_IsInstance(value, (PyObject*)entity_type)) { + PyErr_SetString(PyExc_TypeError, "EntityCollection can only contain Entity objects"); + return -1; + } + + PyUIEntityObject* entity = (PyUIEntityObject*)value; + if (!entity->data) { + PyErr_SetString(PyExc_RuntimeError, "Invalid Entity object"); + return -1; + } + + // Update spatial hash + if (self->grid) { + self->grid->spatial_hash.remove(*it); + } + + // 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; + + // Add to spatial hash + if (self->grid) { + self->grid->spatial_hash.insert(entity->data); + } + + return 0; +} + +int UIEntityCollection::contains(PyUIEntityCollectionObject* self, PyObject* value) { + auto list = self->data.get(); + if (!list) { + PyErr_SetString(PyExc_RuntimeError, "EntityCollection data is null"); + return -1; + } + + // Type checking using cached type + PyTypeObject* entity_type = PyTypeCache::Entity(); + if (!entity_type || !PyObject_IsInstance(value, (PyObject*)entity_type)) { + return 0; // Not an Entity, can't be in collection + } + + PyUIEntityObject* entity = (PyUIEntityObject*)value; + if (!entity->data) { + return 0; + } + + // Search by comparing C++ pointers + for (const auto& ent : *list) { + if (ent.get() == entity->data.get()) { + return 1; + } + } + + return 0; +} + +PyObject* UIEntityCollection::concat(PyUIEntityCollectionObject* self, PyObject* other) { + 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; + } + + PyObject* result_list = PyList_New(self_len + other_len); + if (!result_list) { + return NULL; + } + + PyTypeObject* entity_type = PyTypeCache::Entity(); + if (!entity_type) { + Py_DECREF(result_list); + PyErr_SetString(PyExc_RuntimeError, "Entity type not initialized in cache"); + return NULL; + } + + // Add all elements from self + Py_ssize_t idx = 0; + for (const auto& entity : *self->data) { + auto obj = (PyUIEntityObject*)entity_type->tp_alloc(entity_type, 0); + if (!obj) { + Py_DECREF(result_list); + return NULL; + } + obj->data = entity; + obj->weakreflist = NULL; + PyList_SET_ITEM(result_list, idx++, (PyObject*)obj); + } + + // 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); + } + + 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; + } + + PyTypeObject* entity_type = PyTypeCache::Entity(); + if (!entity_type) { + PyErr_SetString(PyExc_RuntimeError, "Entity type not initialized in cache"); + return NULL; + } + + // First, validate ALL items before modifying anything + Py_ssize_t other_len = PySequence_Length(other); + if (other_len == -1) { + return NULL; + } + + for (Py_ssize_t i = 0; i < other_len; i++) { + PyObject* item = PySequence_GetItem(other, i); + if (!item) { + return NULL; + } + + if (!PyObject_IsInstance(item, (PyObject*)entity_type)) { + 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 safely add them + for (Py_ssize_t i = 0; i < other_len; i++) { + PyObject* item = PySequence_GetItem(other, i); + if (!item) { + return NULL; + } + + PyObject* result = append(self, item); + Py_DECREF(item); + + if (!result) { + return NULL; + } + Py_DECREF(result); + } + + Py_INCREF(self); + return (PyObject*)self; +} + +PySequenceMethods UIEntityCollection::sqmethods = { + .sq_length = (lenfunc)UIEntityCollection::len, + .sq_concat = (binaryfunc)UIEntityCollection::concat, + .sq_repeat = NULL, + .sq_item = (ssizeargfunc)UIEntityCollection::getitem, + .was_sq_slice = NULL, + .sq_ass_item = (ssizeobjargproc)UIEntityCollection::setitem, + .was_sq_ass_slice = NULL, + .sq_contains = (objobjproc)UIEntityCollection::contains, + .sq_inplace_concat = (binaryfunc)UIEntityCollection::inplace_concat, + .sq_inplace_repeat = NULL +}; + +// ============================================================================ +// UIEntityCollection - Mapping protocol (for slice support) +// ============================================================================ + +PyObject* UIEntityCollection::subscript(PyUIEntityCollectionObject* self, PyObject* key) { + if (PyLong_Check(key)) { + Py_ssize_t index = PyLong_AsSsize_t(key); + if (index == -1 && PyErr_Occurred()) { + return NULL; + } + return getitem(self, index); + } else if (PySlice_Check(key)) { + 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; + } + + PyTypeObject* entity_type = PyTypeCache::Entity(); + if (!entity_type) { + Py_DECREF(result_list); + PyErr_SetString(PyExc_RuntimeError, "Entity type not initialized in cache"); + return NULL; + } + + auto it = self->data->begin(); + for (Py_ssize_t i = 0, cur = start; i < slicelength; i++, cur += step) { + auto cur_it = it; + std::advance(cur_it, cur); + + auto obj = (PyUIEntityObject*)entity_type->tp_alloc(entity_type, 0); + if (!obj) { + Py_DECREF(result_list); + return NULL; + } + obj->data = *cur_it; + obj->weakreflist = NULL; + PyList_SET_ITEM(result_list, i, (PyObject*)obj); + } + + return result_list; + } else { + PyErr_Format(PyExc_TypeError, + "EntityCollection indices must be integers or slices, not %.200s", + Py_TYPE(key)->tp_name); + return NULL; + } +} + +int UIEntityCollection::ass_subscript(PyUIEntityCollectionObject* self, PyObject* key, PyObject* value) { + if (PyLong_Check(key)) { + 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)) { + Py_ssize_t start, stop, step, slicelength; + + if (PySlice_GetIndicesEx(key, self->data->size(), &start, &stop, &step, &slicelength) < 0) { + return -1; + } + + // Handle deletion + if (value == NULL) { + if (step == 1) { + // Contiguous slice deletion + auto start_it = self->data->begin(); + std::advance(start_it, start); + auto stop_it = self->data->begin(); + std::advance(stop_it, stop); + + // Clear grid refs and remove from spatial hash + for (auto it = start_it; it != stop_it; ++it) { + if (self->grid) { + self->grid->spatial_hash.remove(*it); + } + (*it)->grid = nullptr; + } + self->data->erase(start_it, stop_it); + } else { + // Extended slice deletion - must delete in reverse to preserve indices + std::vector indices; + for (Py_ssize_t i = 0, cur = start; i < slicelength; i++, cur += step) { + indices.push_back(cur); + } + std::sort(indices.rbegin(), indices.rend()); + + for (Py_ssize_t idx : indices) { + auto it = self->data->begin(); + std::advance(it, idx); + if (self->grid) { + self->grid->spatial_hash.remove(*it); + } + (*it)->grid = nullptr; + self->data->erase(it); + } + } + return 0; + } + + // Handle 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; + } + + PyTypeObject* entity_type = PyTypeCache::Entity(); + if (!entity_type) { + PyErr_SetString(PyExc_RuntimeError, "Entity type not initialized in cache"); + return -1; + } + + // Validate all items first + std::vector> new_items; + for (Py_ssize_t i = 0; i < value_len; i++) { + PyObject* item = PySequence_GetItem(value, i); + if (!item) { + return -1; + } + + if (!PyObject_IsInstance(item, (PyObject*)entity_type)) { + Py_DECREF(item); + PyErr_Format(PyExc_TypeError, + "EntityCollection can only contain Entity objects; got %s", + Py_TYPE(item)->tp_name); + return -1; + } + + PyUIEntityObject* entity_obj = (PyUIEntityObject*)item; + new_items.push_back(entity_obj->data); + Py_DECREF(item); + } + + if (step == 1) { + // Contiguous slice - can change size + auto start_it = self->data->begin(); + std::advance(start_it, start); + auto stop_it = self->data->begin(); + std::advance(stop_it, stop); + + // Clear old grid refs and remove from spatial hash + for (auto it = start_it; it != stop_it; ++it) { + if (self->grid) { + self->grid->spatial_hash.remove(*it); + } + (*it)->grid = nullptr; + } + + // Erase old range + auto insert_pos = self->data->erase(start_it, stop_it); + + // Insert new items + for (const auto& entity : new_items) { + self->data->insert(insert_pos, entity); + entity->grid = self->grid; + if (self->grid) { + self->grid->spatial_hash.insert(entity); + } + } + } else { + // Extended slice - must match size + 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; + } + + auto it = self->data->begin(); + size_t new_idx = 0; + for (Py_ssize_t i = 0, cur = start; i < slicelength; i++, cur += step) { + auto cur_it = it; + std::advance(cur_it, cur); + + if (self->grid) { + self->grid->spatial_hash.remove(*cur_it); + } + (*cur_it)->grid = nullptr; + + *cur_it = new_items[new_idx++]; + (*cur_it)->grid = self->grid; + + if (self->grid) { + self->grid->spatial_hash.insert(*cur_it); + } + } + } + + return 0; + } else { + PyErr_Format(PyExc_TypeError, + "EntityCollection indices must be integers or slices, not %.200s", + Py_TYPE(key)->tp_name); + return -1; + } +} + +PyMappingMethods UIEntityCollection::mpmethods = { + .mp_length = (lenfunc)UIEntityCollection::len, + .mp_subscript = (binaryfunc)UIEntityCollection::subscript, + .mp_ass_subscript = (objobjargproc)UIEntityCollection::ass_subscript +}; + +// ============================================================================ +// UIEntityCollection - Methods +// ============================================================================ + +PyObject* UIEntityCollection::append(PyUIEntityCollectionObject* self, PyObject* o) +{ + PyTypeObject* entity_type = PyTypeCache::Entity(); + if (!entity_type) { + PyErr_SetString(PyExc_RuntimeError, "Entity type not initialized in cache"); + return NULL; + } + + if (!PyObject_IsInstance(o, (PyObject*)entity_type)) { + PyErr_SetString(PyExc_TypeError, "Only Entity objects can be added to EntityCollection"); + return NULL; + } + + PyUIEntityObject* entity = (PyUIEntityObject*)o; + + // Remove from old grid first (if different from target grid) + if (entity->data->grid && entity->data->grid != self->grid) { + auto old_grid = entity->data->grid; + auto& old_entities = old_grid->entities; + auto it = std::find_if(old_entities->begin(), old_entities->end(), + [entity](const std::shared_ptr& e) { + return e.get() == entity->data.get(); + }); + if (it != old_entities->end()) { + old_entities->erase(it); + old_grid->spatial_hash.remove(entity->data); + } + } + + // Add to this grid (if not already in it) + if (entity->data->grid != self->grid) { + self->data->push_back(entity->data); + entity->data->grid = self->grid; + if (self->grid) { + self->grid->spatial_hash.insert(entity->data); + } + } + + // Initialize gridstate if not already done + if (entity->data->gridstate.size() == 0 && self->grid) { + entity->data->gridstate.resize(self->grid->grid_w * self->grid->grid_h); + for (auto& state : entity->data->gridstate) { + state.visible = false; + state.discovered = false; + } + } + + Py_RETURN_NONE; +} + +PyObject* UIEntityCollection::remove(PyUIEntityCollectionObject* self, PyObject* o) +{ + PyTypeObject* entity_type = PyTypeCache::Entity(); + if (!entity_type) { + PyErr_SetString(PyExc_RuntimeError, "Entity type not initialized in cache"); + return NULL; + } + + if (!PyObject_IsInstance(o, (PyObject*)entity_type)) { + PyErr_SetString(PyExc_TypeError, "EntityCollection.remove requires an Entity object"); + return NULL; + } + + PyUIEntityObject* entity = (PyUIEntityObject*)o; + if (!entity->data) { + PyErr_SetString(PyExc_RuntimeError, "Invalid Entity object"); + return NULL; + } + + auto list = self->data.get(); + if (!list) { + PyErr_SetString(PyExc_RuntimeError, "EntityCollection data is null"); + return NULL; + } + + // Search by comparing C++ pointers + for (auto it = list->begin(); it != list->end(); ++it) { + if (it->get() == entity->data.get()) { + if (self->grid) { + self->grid->spatial_hash.remove(*it); + } + (*it)->grid = nullptr; + list->erase(it); + Py_RETURN_NONE; + } + } + + PyErr_SetString(PyExc_ValueError, "Entity not in EntityCollection"); + return NULL; +} + +PyObject* UIEntityCollection::extend(PyUIEntityCollectionObject* self, PyObject* o) +{ + // Get iterator for the input + PyObject* iterator = PyObject_GetIter(o); + if (!iterator) { + PyErr_SetString(PyExc_TypeError, "EntityCollection.extend requires an iterable"); + return NULL; + } + + PyTypeObject* entity_type = PyTypeCache::Entity(); + if (!entity_type) { + Py_DECREF(iterator); + PyErr_SetString(PyExc_RuntimeError, "Entity type not initialized in cache"); + return NULL; + } + + // FIXED: Validate ALL items first before modifying anything + // (Following the pattern from inplace_concat) + std::vector validated_entities; + PyObject* item; + while ((item = PyIter_Next(iterator)) != NULL) { + if (!PyObject_IsInstance(item, (PyObject*)entity_type)) { + // Cleanup on error + Py_DECREF(item); + Py_DECREF(iterator); + PyErr_SetString(PyExc_TypeError, "All items in iterable must be Entity objects"); + return NULL; + } + + PyUIEntityObject* entity = (PyUIEntityObject*)item; + if (!entity->data) { + Py_DECREF(item); + Py_DECREF(iterator); + PyErr_SetString(PyExc_RuntimeError, "Invalid Entity object in iterable"); + return NULL; + } + + validated_entities.push_back(entity); + // Note: We keep the reference to item until after we're done + } + + Py_DECREF(iterator); + + // Check if iteration ended due to an error + if (PyErr_Occurred()) { + // Release all held references + for (auto* ent : validated_entities) { + Py_DECREF(ent); + } + return NULL; + } + + // All items validated - now we can safely add them + for (auto* entity : validated_entities) { + self->data->push_back(entity->data); + entity->data->grid = self->grid; + + if (self->grid) { + self->grid->spatial_hash.insert(entity->data); + } + + // Initialize gridstate if needed + if (entity->data->gridstate.size() == 0 && self->grid) { + entity->data->gridstate.resize(self->grid->grid_w * self->grid->grid_h); + for (auto& state : entity->data->gridstate) { + state.visible = false; + state.discovered = false; + } + } + + Py_DECREF(entity); // Release the reference we held during validation + } + + Py_RETURN_NONE; +} + +PyObject* UIEntityCollection::pop(PyUIEntityCollectionObject* self, PyObject* args) +{ + Py_ssize_t index = -1; + + if (!PyArg_ParseTuple(args, "|n", &index)) { + return NULL; + } + + auto list = self->data.get(); + if (!list) { + PyErr_SetString(PyExc_RuntimeError, "EntityCollection data is null"); + return NULL; + } + + if (list->empty()) { + PyErr_SetString(PyExc_IndexError, "pop from empty EntityCollection"); + return NULL; + } + + // Handle negative indexing + Py_ssize_t size = static_cast(list->size()); + if (index < 0) { + index += size; + } + + if (index < 0 || index >= size) { + PyErr_SetString(PyExc_IndexError, "pop index out of range"); + return NULL; + } + + auto it = list->begin(); + std::advance(it, index); + + std::shared_ptr entity = *it; + + // Remove from spatial hash and clear grid reference + if (self->grid) { + self->grid->spatial_hash.remove(entity); + } + entity->grid = nullptr; + list->erase(it); + + // Create Python object for the entity + PyTypeObject* entity_type = PyTypeCache::Entity(); + if (!entity_type) { + PyErr_SetString(PyExc_RuntimeError, "Entity type not initialized in cache"); + return NULL; + } + + PyUIEntityObject* py_entity = (PyUIEntityObject*)entity_type->tp_alloc(entity_type, 0); + if (!py_entity) { + return NULL; + } + + py_entity->data = entity; + py_entity->weakreflist = NULL; + + return (PyObject*)py_entity; +} + +PyObject* UIEntityCollection::insert(PyUIEntityCollectionObject* self, PyObject* args) +{ + Py_ssize_t index; + PyObject* o; + + if (!PyArg_ParseTuple(args, "nO", &index, &o)) { + return NULL; + } + + auto list = self->data.get(); + if (!list) { + PyErr_SetString(PyExc_RuntimeError, "EntityCollection data is null"); + return NULL; + } + + PyTypeObject* entity_type = PyTypeCache::Entity(); + if (!entity_type) { + PyErr_SetString(PyExc_RuntimeError, "Entity type not initialized in cache"); + return NULL; + } + + if (!PyObject_IsInstance(o, (PyObject*)entity_type)) { + PyErr_SetString(PyExc_TypeError, "EntityCollection.insert requires an Entity object"); + return NULL; + } + + PyUIEntityObject* entity = (PyUIEntityObject*)o; + if (!entity->data) { + PyErr_SetString(PyExc_RuntimeError, "Invalid Entity object"); + return NULL; + } + + // Handle negative indexing and clamping (Python list.insert behavior) + Py_ssize_t size = static_cast(list->size()); + if (index < 0) { + index += size; + if (index < 0) { + index = 0; + } + } else if (index > size) { + index = size; + } + + auto it = list->begin(); + std::advance(it, index); + + list->insert(it, entity->data); + entity->data->grid = self->grid; + + if (self->grid) { + self->grid->spatial_hash.insert(entity->data); + } + + // Initialize gridstate if needed + if (entity->data->gridstate.size() == 0 && self->grid) { + entity->data->gridstate.resize(self->grid->grid_w * self->grid->grid_h); + for (auto& state : entity->data->gridstate) { + state.visible = false; + state.discovered = false; + } + } + + Py_RETURN_NONE; +} + +PyObject* UIEntityCollection::index_method(PyUIEntityCollectionObject* self, PyObject* value) { + auto list = self->data.get(); + if (!list) { + PyErr_SetString(PyExc_RuntimeError, "EntityCollection data is null"); + return NULL; + } + + PyTypeObject* entity_type = PyTypeCache::Entity(); + if (!entity_type) { + PyErr_SetString(PyExc_RuntimeError, "Entity type not initialized in cache"); + return NULL; + } + + if (!PyObject_IsInstance(value, (PyObject*)entity_type)) { + PyErr_SetString(PyExc_TypeError, "EntityCollection.index requires an Entity object"); + return NULL; + } + + PyUIEntityObject* entity = (PyUIEntityObject*)value; + if (!entity->data) { + PyErr_SetString(PyExc_RuntimeError, "Invalid Entity object"); + return NULL; + } + + Py_ssize_t idx = 0; + for (const auto& ent : *list) { + if (ent.get() == entity->data.get()) { + return PyLong_FromSsize_t(idx); + } + idx++; + } + + PyErr_SetString(PyExc_ValueError, "Entity not in EntityCollection"); + return NULL; +} + +PyObject* UIEntityCollection::count(PyUIEntityCollectionObject* self, PyObject* value) { + auto list = self->data.get(); + if (!list) { + PyErr_SetString(PyExc_RuntimeError, "EntityCollection data is null"); + return NULL; + } + + PyTypeObject* entity_type = PyTypeCache::Entity(); + if (!entity_type || !PyObject_IsInstance(value, (PyObject*)entity_type)) { + return PyLong_FromLong(0); + } + + PyUIEntityObject* entity = (PyUIEntityObject*)value; + if (!entity->data) { + return PyLong_FromLong(0); + } + + Py_ssize_t cnt = 0; + for (const auto& ent : *list) { + if (ent.get() == entity->data.get()) { + cnt++; + } + } + + return PyLong_FromSsize_t(cnt); +} + +// Helper function for entity name matching with wildcards +static bool matchEntityName(const std::string& name, const std::string& pattern) { + if (pattern.find('*') != std::string::npos) { + if (pattern == "*") { + return true; + } else if (pattern.front() == '*' && pattern.back() == '*' && pattern.length() > 2) { + std::string substring = pattern.substr(1, pattern.length() - 2); + return name.find(substring) != std::string::npos; + } else if (pattern.front() == '*') { + std::string suffix = pattern.substr(1); + return name.length() >= suffix.length() && + name.compare(name.length() - suffix.length(), suffix.length(), suffix) == 0; + } else if (pattern.back() == '*') { + std::string prefix = pattern.substr(0, pattern.length() - 1); + return name.compare(0, prefix.length(), prefix) == 0; + } + return name == pattern; + } + return name == pattern; +} + +PyObject* UIEntityCollection::find(PyUIEntityCollectionObject* self, PyObject* args, PyObject* kwds) { + const char* name = nullptr; + + static const char* kwlist[] = {"name", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "s", const_cast(kwlist), &name)) { + return NULL; + } + + auto list = self->data.get(); + if (!list) { + PyErr_SetString(PyExc_RuntimeError, "EntityCollection data is null"); + return NULL; + } + + std::string pattern(name); + bool has_wildcard = (pattern.find('*') != std::string::npos); + + PyTypeObject* entity_type = PyTypeCache::Entity(); + if (!entity_type) { + PyErr_SetString(PyExc_RuntimeError, "Entity type not initialized in cache"); + return NULL; + } + + if (has_wildcard) { + PyObject* results = PyList_New(0); + if (!results) { + return NULL; + } + + for (auto& entity : *list) { + if (matchEntityName(entity->sprite.name, pattern)) { + PyUIEntityObject* py_entity = (PyUIEntityObject*)entity_type->tp_alloc(entity_type, 0); + if (!py_entity) { + Py_DECREF(results); + return NULL; + } + py_entity->data = entity; + py_entity->weakreflist = NULL; + + if (PyList_Append(results, (PyObject*)py_entity) < 0) { + Py_DECREF(py_entity); + Py_DECREF(results); + return NULL; + } + Py_DECREF(py_entity); + } + } + + return results; + } else { + for (auto& entity : *list) { + if (entity->sprite.name == pattern) { + PyUIEntityObject* py_entity = (PyUIEntityObject*)entity_type->tp_alloc(entity_type, 0); + if (!py_entity) { + return NULL; + } + py_entity->data = entity; + py_entity->weakreflist = NULL; + return (PyObject*)py_entity; + } + } + + Py_RETURN_NONE; + } +} + +PyMethodDef UIEntityCollection::methods[] = { + {"append", (PyCFunction)UIEntityCollection::append, METH_O, + "append(entity)\n\n" + "Add an entity to the end of the collection."}, + {"extend", (PyCFunction)UIEntityCollection::extend, METH_O, + "extend(iterable)\n\n" + "Add all entities from an iterable to the collection."}, + {"insert", (PyCFunction)UIEntityCollection::insert, METH_VARARGS, + "insert(index, entity)\n\n" + "Insert entity at index. Like list.insert(), indices past the end append."}, + {"remove", (PyCFunction)UIEntityCollection::remove, METH_O, + "remove(entity)\n\n" + "Remove first occurrence of entity. Raises ValueError if not found."}, + {"pop", (PyCFunction)UIEntityCollection::pop, METH_VARARGS, + "pop([index]) -> entity\n\n" + "Remove and return entity at index (default: last entity)."}, + {"index", (PyCFunction)UIEntityCollection::index_method, METH_O, + "index(entity) -> int\n\n" + "Return index of first occurrence of entity. Raises ValueError if not found."}, + {"count", (PyCFunction)UIEntityCollection::count, METH_O, + "count(entity) -> int\n\n" + "Count occurrences of entity in the collection."}, + {"find", (PyCFunction)UIEntityCollection::find, METH_VARARGS | METH_KEYWORDS, + "find(name) -> entity or list\n\n" + "Find entities by name.\n\n" + "Args:\n" + " name (str): Name to search for. Supports wildcards:\n" + " - 'exact' for exact match (returns single entity or None)\n" + " - 'prefix*' for starts-with match (returns list)\n" + " - '*suffix' for ends-with match (returns list)\n" + " - '*substring*' for contains match (returns list)\n\n" + "Returns:\n" + " Single entity if exact match, list if wildcard, None if not found."}, + {NULL, NULL, 0, NULL} +}; + +PyObject* UIEntityCollection::repr(PyUIEntityCollectionObject* self) +{ + std::ostringstream ss; + if (!self->data) { + ss << ""; + } else { + ss << "data->size() << " entities)>"; + } + std::string repr_str = ss.str(); + return PyUnicode_DecodeUTF8(repr_str.c_str(), repr_str.size(), "replace"); +} + +int UIEntityCollection::init(PyUIEntityCollectionObject* self, PyObject* args, PyObject* kwds) +{ + PyErr_SetString(PyExc_TypeError, "EntityCollection cannot be instantiated: a C++ data source is required."); + return -1; +} + +PyObject* UIEntityCollection::iter(PyUIEntityCollectionObject* self) +{ + PyTypeObject* iterType = &mcrfpydef::PyUIEntityCollectionIterType; + + PyUIEntityCollectionIterObject* iterObj = (PyUIEntityCollectionIterObject*)iterType->tp_alloc(iterType, 0); + if (!iterObj) { + return NULL; + } + + iterObj->data = self->data; + iterObj->current = self->data->begin(); + iterObj->end = self->data->end(); + iterObj->start_size = self->data->size(); + + return (PyObject*)iterObj; +} diff --git a/src/UIEntityCollection.h b/src/UIEntityCollection.h new file mode 100644 index 0000000..f893b66 --- /dev/null +++ b/src/UIEntityCollection.h @@ -0,0 +1,145 @@ +#pragma once +// UIEntityCollection.h - Collection type for managing entities on a grid +// +// Extracted from UIGrid.cpp as part of code organization cleanup. +// This is a Python sequence/mapping type that wraps std::list> +// with grid-aware semantics (entities can only belong to one grid at a time). + +#include "Common.h" +#include "Python.h" +#include "structmember.h" +#include +#include + +// Forward declarations +class UIEntity; +class UIGrid; + +// Python object for EntityCollection +typedef struct { + PyObject_HEAD + std::shared_ptr>> data; + std::shared_ptr grid; +} PyUIEntityCollectionObject; + +// Python object for EntityCollection iterator +typedef struct { + PyObject_HEAD + std::shared_ptr>> data; + std::list>::iterator current; // Actual list iterator - O(1) increment + std::list>::iterator end; // End iterator for bounds check + int start_size; // For detecting modification during iteration +} PyUIEntityCollectionIterObject; + +// UIEntityCollection - Python collection wrapper +class UIEntityCollection { +public: + // Python sequence protocol + static PySequenceMethods sqmethods; + static PyMappingMethods mpmethods; + + // Collection methods + static PyObject* append(PyUIEntityCollectionObject* self, PyObject* o); + static PyObject* extend(PyUIEntityCollectionObject* self, PyObject* o); + static PyObject* remove(PyUIEntityCollectionObject* self, PyObject* o); + static PyObject* pop(PyUIEntityCollectionObject* self, PyObject* args); + static PyObject* insert(PyUIEntityCollectionObject* self, PyObject* args); + static PyObject* index_method(PyUIEntityCollectionObject* self, PyObject* value); + static PyObject* count(PyUIEntityCollectionObject* self, PyObject* value); + static PyObject* find(PyUIEntityCollectionObject* self, PyObject* args, PyObject* kwds); + static PyMethodDef methods[]; + + // Python type slots + static PyObject* repr(PyUIEntityCollectionObject* self); + static int init(PyUIEntityCollectionObject* self, PyObject* args, PyObject* kwds); + static PyObject* iter(PyUIEntityCollectionObject* self); + + // Sequence methods + static Py_ssize_t len(PyUIEntityCollectionObject* self); + static PyObject* getitem(PyUIEntityCollectionObject* self, Py_ssize_t index); + static int setitem(PyUIEntityCollectionObject* self, Py_ssize_t index, PyObject* value); + static int contains(PyUIEntityCollectionObject* self, PyObject* value); + static PyObject* concat(PyUIEntityCollectionObject* self, PyObject* other); + static PyObject* inplace_concat(PyUIEntityCollectionObject* self, PyObject* other); + + // Mapping methods (for slice support) + static PyObject* subscript(PyUIEntityCollectionObject* self, PyObject* key); + static int ass_subscript(PyUIEntityCollectionObject* self, PyObject* key, PyObject* value); +}; + +// UIEntityCollectionIter - Iterator for EntityCollection +class UIEntityCollectionIter { +public: + static int init(PyUIEntityCollectionIterObject* self, PyObject* args, PyObject* kwds); + static PyObject* next(PyUIEntityCollectionIterObject* self); + static PyObject* repr(PyUIEntityCollectionIterObject* self); +}; + +// Python type objects - defined in mcrfpydef namespace +namespace mcrfpydef { + + // Iterator type + inline PyTypeObject PyUIEntityCollectionIterType = { + .ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0}, + .tp_name = "mcrfpy.UIEntityCollectionIter", + .tp_basicsize = sizeof(PyUIEntityCollectionIterObject), + .tp_itemsize = 0, + .tp_dealloc = (destructor)[](PyObject* self) + { + PyUIEntityCollectionIterObject* obj = (PyUIEntityCollectionIterObject*)self; + obj->data.reset(); + Py_TYPE(self)->tp_free(self); + }, + .tp_repr = (reprfunc)UIEntityCollectionIter::repr, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_doc = PyDoc_STR("Iterator for a collection of Entity objects"), + .tp_iter = PyObject_SelfIter, + .tp_iternext = (iternextfunc)UIEntityCollectionIter::next, + .tp_init = (initproc)UIEntityCollectionIter::init, + .tp_alloc = PyType_GenericAlloc, + .tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject* + { + PyErr_SetString(PyExc_TypeError, "UIEntityCollectionIter cannot be instantiated directly"); + return NULL; + } + }; + + // Collection type + inline PyTypeObject PyUIEntityCollectionType = { + .ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0}, + .tp_name = "mcrfpy.EntityCollection", + .tp_basicsize = sizeof(PyUIEntityCollectionObject), + .tp_itemsize = 0, + .tp_dealloc = (destructor)[](PyObject* self) + { + PyUIEntityCollectionObject* obj = (PyUIEntityCollectionObject*)self; + obj->data.reset(); + obj->grid.reset(); + Py_TYPE(self)->tp_free(self); + }, + .tp_repr = (reprfunc)UIEntityCollection::repr, + .tp_as_sequence = &UIEntityCollection::sqmethods, + .tp_as_mapping = &UIEntityCollection::mpmethods, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_doc = PyDoc_STR("Iterable, indexable collection of Entity objects.\n\n" + "EntityCollection manages entities that belong to a Grid. " + "Entities can only belong to one grid at a time - adding an entity " + "to a new grid automatically removes it from its previous grid.\n\n" + "Supports list-like operations: indexing, slicing, append, extend, " + "remove, pop, insert, index, count, and find.\n\n" + "Example:\n" + " grid.entities.append(entity)\n" + " player = grid.entities.find(name='player')\n" + " for entity in grid.entities:\n" + " print(entity.pos)"), + .tp_iter = (getiterfunc)UIEntityCollection::iter, + .tp_methods = UIEntityCollection::methods, + .tp_init = (initproc)UIEntityCollection::init, + .tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject* + { + PyErr_SetString(PyExc_TypeError, "EntityCollection cannot be instantiated: a C++ data source is required."); + return NULL; + } + }; + +} // namespace mcrfpydef diff --git a/src/UIGrid.cpp b/src/UIGrid.cpp index ddf0940..35a254e 100644 --- a/src/UIGrid.cpp +++ b/src/UIGrid.cpp @@ -2,6 +2,7 @@ #include "GameEngine.h" #include "McRFPy_API.h" #include "PythonObjectCache.h" +#include "PyTypeCache.h" // Thread-safe cached Python types #include "UIEntity.h" #include "Profiler.h" #include "PyFOV.h" @@ -12,6 +13,7 @@ #include // #150 - for strcmp #include // #169 - for std::numeric_limits // UIDrawable methods now in UIBase.h +// UIEntityCollection code moved to UIEntityCollection.cpp UIGrid::UIGrid() : grid_w(0), grid_h(0), zoom(1.0f), center_x(0.0f), center_y(0.0f), ptex(nullptr), @@ -1210,6 +1212,54 @@ PyObject* UIGrid::py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds) return (PyObject*)obj; } +// Grid subscript access: grid[x, y] -> GridPoint +// Enables Pythonic cell access syntax +PyObject* UIGrid::subscript(PyUIGridObject* self, PyObject* key) +{ + // We expect a tuple of (x, y) + if (!PyTuple_Check(key) || PyTuple_Size(key) != 2) { + PyErr_SetString(PyExc_TypeError, "Grid indices must be a tuple of (x, y)"); + return NULL; + } + + PyObject* x_obj = PyTuple_GetItem(key, 0); + PyObject* y_obj = PyTuple_GetItem(key, 1); + + if (!PyLong_Check(x_obj) || !PyLong_Check(y_obj)) { + PyErr_SetString(PyExc_TypeError, "Grid indices must be integers"); + return NULL; + } + + int x = PyLong_AsLong(x_obj); + int y = PyLong_AsLong(y_obj); + + // Range validation + if (x < 0 || x >= self->data->grid_w) { + PyErr_Format(PyExc_IndexError, "x index %d is out of range [0, %d)", x, self->data->grid_w); + return NULL; + } + if (y < 0 || y >= self->data->grid_h) { + PyErr_Format(PyExc_IndexError, "y index %d is out of range [0, %d)", y, self->data->grid_h); + return NULL; + } + + // Create GridPoint object (same as py_at) + auto type = &mcrfpydef::PyUIGridPointType; + auto obj = (PyUIGridPointObject*)type->tp_alloc(type, 0); + if (!obj) return NULL; + + obj->data = &(self->data->at(x, y)); + obj->grid = self->data; + return (PyObject*)obj; +} + +// Mapping methods for grid[x, y] subscript access +PyMappingMethods UIGrid::mpmethods = { + .mp_length = NULL, // No len() for grid via mapping (use grid_w * grid_h) + .mp_subscript = (binaryfunc)UIGrid::subscript, + .mp_ass_subscript = NULL // No assignment via subscript (use grid[x,y].property = value) +}; + PyObject* UIGrid::get_fill_color(PyUIGridObject* self, void* closure) { auto& color = self->data->fill_color; @@ -2125,7 +2175,8 @@ PyGetSetDef UIGrid::getsetters[] = { PyObject* UIGrid::get_entities(PyUIGridObject* self, void* closure) { // Returns EntityCollection for entity management - auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "EntityCollection"); + // Use the type directly from namespace (type not exported to module) + PyTypeObject* type = &mcrfpydef::PyUIEntityCollectionType; auto o = (PyUIEntityCollectionObject*)type->tp_alloc(type, 0); if (o) { o->data = self->data->entities; @@ -2326,992 +2377,7 @@ void UIGrid::updateCellHover(sf::Vector2f mousepos) { } } -int UIEntityCollectionIter::init(PyUIEntityCollectionIterObject* self, PyObject* args, PyObject* kwds) -{ - PyErr_SetString(PyExc_TypeError, "UICollection cannot be instantiated: a C++ data source is required."); - return -1; -} - -PyObject* UIEntityCollectionIter::next(PyUIEntityCollectionIterObject* self) -{ - // Check for collection modification during iteration - if (self->data->size() != self->start_size) - { - PyErr_SetString(PyExc_RuntimeError, "collection changed size during iteration"); - return NULL; - } - - // Check if we've reached the end - if (self->current == self->end) - { - PyErr_SetNone(PyExc_StopIteration); - return NULL; - } - - // Get current element and advance iterator - O(1) operation - auto target = *self->current; - ++self->current; - - // 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 - // Cache the Entity type to avoid repeated dictionary lookups (#159) - static PyTypeObject* cached_entity_type = nullptr; - if (!cached_entity_type) { - cached_entity_type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"); - } - auto o = (PyUIEntityObject*)cached_entity_type->tp_alloc(cached_entity_type, 0); - auto p = std::static_pointer_cast(target); - o->data = p; - return (PyObject*)o; -} - -PyObject* UIEntityCollectionIter::repr(PyUIEntityCollectionIterObject* self) -{ - std::ostringstream ss; - if (!self->data) ss << ""; - else { - // Calculate current position by distance from end - auto remaining = std::distance(self->current, self->end); - auto total = self->data->size(); - ss << ""; - } - std::string repr_str = ss.str(); - return PyUnicode_DecodeUTF8(repr_str.c_str(), repr_str.size(), "replace"); -} - -Py_ssize_t UIEntityCollection::len(PyUIEntityCollectionObject* self) { - return self->data->size(); -} - -PyObject* UIEntityCollection::getitem(PyUIEntityCollectionObject* self, Py_ssize_t index) { - // build a Python version of item at self->data[index] - // Copy pasted:: - auto vec = self->data.get(); - if (!vec) - { - PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer"); - return NULL; - } - while (index < 0) index += self->data->size(); - if (index > self->data->size() - 1) - { - PyErr_SetString(PyExc_IndexError, "EntityCollection index out of range"); - return NULL; - } - auto l_begin = (*vec).begin(); - std::advance(l_begin, index); - auto target = *l_begin; //auto target = (*vec)[index]; - - // Check cache first to preserve derived class - if (target->serial_number != 0) { - PyObject* cached = PythonObjectCache::getInstance().lookup(target->serial_number); - if (cached) { - return cached; // Already INCREF'd by lookup - } - } - - // Legacy: 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(target); - o->data = p; - return (PyObject*)o; -} - -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; -} - - -PySequenceMethods UIEntityCollection::sqmethods = { - .sq_length = (lenfunc)UIEntityCollection::len, - .sq_concat = (binaryfunc)UIEntityCollection::concat, - .sq_repeat = NULL, - .sq_item = (ssizeargfunc)UIEntityCollection::getitem, - .was_sq_slice = NULL, - .sq_ass_item = (ssizeobjargproc)UIEntityCollection::setitem, - .was_sq_ass_slice = NULL, - .sq_contains = (objobjproc)UIEntityCollection::contains, - .sq_inplace_concat = (binaryfunc)UIEntityCollection::inplace_concat, - .sq_inplace_repeat = NULL -}; - -PyObject* UIEntityCollection::append(PyUIEntityCollectionObject* self, PyObject* o) -{ - // Type check - must be Entity - if (!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"))) - { - PyErr_SetString(PyExc_TypeError, "Only Entity objects can be added to EntityCollection"); - return NULL; - } - PyUIEntityObject* entity = (PyUIEntityObject*)o; - - // Remove from old grid first (if different from target grid) - // This implements the documented "single grid only" behavior - if (entity->data->grid && entity->data->grid != self->grid) { - auto old_grid = entity->data->grid; - auto& old_entities = old_grid->entities; - auto it = std::find_if(old_entities->begin(), old_entities->end(), - [entity](const std::shared_ptr& e) { - return e.get() == entity->data.get(); - }); - if (it != old_entities->end()) { - old_entities->erase(it); - // Remove from old grid's spatial hash (#115) - old_grid->spatial_hash.remove(entity->data); - } - } - - // Add to this grid (if not already in it) - if (entity->data->grid != self->grid) { - self->data->push_back(entity->data); - entity->data->grid = self->grid; - // Add to spatial hash for O(1) queries (#115) - if (self->grid) { - self->grid->spatial_hash.insert(entity->data); - } - } - - // Initialize gridstate if not already done - if (entity->data->gridstate.size() == 0 && self->grid) { - entity->data->gridstate.resize(self->grid->grid_w * self->grid->grid_h); - // Initialize all cells as not visible/discovered - for (auto& state : entity->data->gridstate) { - state.visible = false; - state.discovered = false; - } - } - - Py_INCREF(Py_None); - return Py_None; -} - -PyObject* UIEntityCollection::remove(PyUIEntityCollectionObject* self, PyObject* o) -{ - // Type checking - must be an Entity - if (!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"))) - { - PyErr_SetString(PyExc_TypeError, "EntityCollection.remove requires an Entity object"); - return NULL; - } - - // Get the C++ object from the Python object - PyUIEntityObject* entity = (PyUIEntityObject*)o; - if (!entity->data) { - PyErr_SetString(PyExc_RuntimeError, "Invalid Entity object"); - return NULL; - } - - // Get the underlying list - auto list = self->data.get(); - if (!list) { - PyErr_SetString(PyExc_RuntimeError, "The collection store returned a null pointer"); - return NULL; - } - - // Search for the entity by comparing C++ pointers - auto it = list->begin(); - while (it != list->end()) { - if (it->get() == entity->data.get()) { - // Remove from spatial hash before clearing grid (#115) - if (self->grid) { - self->grid->spatial_hash.remove(*it); - } - - // Found it - clear grid reference before removing - (*it)->grid = nullptr; - - // Remove from the list - self->data->erase(it); - - Py_INCREF(Py_None); - return Py_None; - } - ++it; - } - - // Entity not found - raise ValueError - PyErr_SetString(PyExc_ValueError, "Entity not in EntityCollection"); - return NULL; -} - -PyObject* UIEntityCollection::extend(PyUIEntityCollectionObject* self, PyObject* o) -{ - // Accept any iterable of Entity objects - PyObject* iterator = PyObject_GetIter(o); - if (iterator == NULL) { - PyErr_SetString(PyExc_TypeError, "UIEntityCollection.extend requires an iterable"); - return NULL; - } - - PyObject* item; - while ((item = PyIter_Next(iterator)) != NULL) { - // Check if item is an Entity - if (!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"))) { - Py_DECREF(item); - Py_DECREF(iterator); - PyErr_SetString(PyExc_TypeError, "All items in iterable must be Entity objects"); - return NULL; - } - - // Add the entity to the collection - PyUIEntityObject* entity = (PyUIEntityObject*)item; - self->data->push_back(entity->data); - entity->data->grid = self->grid; - - Py_DECREF(item); - } - - Py_DECREF(iterator); - - // Check if iteration ended due to an error - if (PyErr_Occurred()) { - return NULL; - } - - Py_INCREF(Py_None); - return Py_None; -} - -PyObject* UIEntityCollection::pop(PyUIEntityCollectionObject* self, PyObject* args) -{ - Py_ssize_t index = -1; // Default to last element - - if (!PyArg_ParseTuple(args, "|n", &index)) { - return NULL; - } - - auto list = self->data.get(); - if (!list) { - PyErr_SetString(PyExc_RuntimeError, "Collection data is null"); - return NULL; - } - - if (list->empty()) { - PyErr_SetString(PyExc_IndexError, "pop from empty EntityCollection"); - return NULL; - } - - // Handle negative indexing - Py_ssize_t size = static_cast(list->size()); - if (index < 0) { - index += size; - } - - if (index < 0 || index >= size) { - PyErr_SetString(PyExc_IndexError, "pop index out of range"); - return NULL; - } - - // Navigate to the element (std::list requires iteration) - auto it = list->begin(); - std::advance(it, index); - - // Get the entity before removing - std::shared_ptr entity = *it; - - // Clear grid reference and remove from list - entity->grid = nullptr; - list->erase(it); - - // Create Python object for the entity - PyTypeObject* entityType = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"); - if (!entityType) { - PyErr_SetString(PyExc_RuntimeError, "Could not find Entity type"); - return NULL; - } - - PyUIEntityObject* py_entity = (PyUIEntityObject*)entityType->tp_alloc(entityType, 0); - Py_DECREF(entityType); - - if (!py_entity) { - return NULL; - } - - py_entity->data = entity; - py_entity->weakreflist = NULL; - - return (PyObject*)py_entity; -} - -PyObject* UIEntityCollection::insert(PyUIEntityCollectionObject* self, PyObject* args) -{ - Py_ssize_t index; - PyObject* o; - - if (!PyArg_ParseTuple(args, "nO", &index, &o)) { - return NULL; - } - - auto list = self->data.get(); - if (!list) { - PyErr_SetString(PyExc_RuntimeError, "Collection data is null"); - return NULL; - } - - // Type checking - must be an Entity - if (!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"))) { - PyErr_SetString(PyExc_TypeError, "EntityCollection.insert requires an Entity object"); - return NULL; - } - - PyUIEntityObject* entity = (PyUIEntityObject*)o; - if (!entity->data) { - PyErr_SetString(PyExc_RuntimeError, "Invalid Entity object"); - return NULL; - } - - // Handle negative indexing and clamping (Python list.insert behavior) - Py_ssize_t size = static_cast(list->size()); - if (index < 0) { - index += size; - if (index < 0) { - index = 0; - } - } else if (index > size) { - index = size; - } - - // Navigate to insert position - auto it = list->begin(); - std::advance(it, index); - - // Insert and set grid reference - list->insert(it, entity->data); - entity->data->grid = self->grid; - - // Initialize gridstate if needed - if (entity->data->gridstate.size() == 0 && self->grid) { - entity->data->gridstate.resize(self->grid->grid_w * self->grid->grid_h); - for (auto& state : entity->data->gridstate) { - state.visible = false; - state.discovered = false; - } - } - - Py_RETURN_NONE; -} - -PyObject* UIEntityCollection::index_method(PyUIEntityCollectionObject* self, PyObject* value) { - auto list = self->data.get(); - if (!list) { - PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer"); - return NULL; - } - - // Type checking - must be an Entity - if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"))) { - PyErr_SetString(PyExc_TypeError, "EntityCollection.index requires an Entity object"); - return NULL; - } - - // Get the C++ object from the Python object - PyUIEntityObject* entity = (PyUIEntityObject*)value; - if (!entity->data) { - PyErr_SetString(PyExc_RuntimeError, "Invalid Entity object"); - return NULL; - } - - // Search for the object - Py_ssize_t idx = 0; - for (const auto& ent : *list) { - if (ent.get() == entity->data.get()) { - return PyLong_FromSsize_t(idx); - } - idx++; - } - - PyErr_SetString(PyExc_ValueError, "Entity not in EntityCollection"); - return NULL; -} - -PyObject* UIEntityCollection::count(PyUIEntityCollectionObject* self, PyObject* value) { - auto list = self->data.get(); - if (!list) { - PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer"); - return NULL; - } - - // Type checking - must be an Entity - if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"))) { - // Not an Entity, so count is 0 - return PyLong_FromLong(0); - } - - // Get the C++ object from the Python object - PyUIEntityObject* entity = (PyUIEntityObject*)value; - if (!entity->data) { - return PyLong_FromLong(0); - } - - // Count occurrences - Py_ssize_t count = 0; - for (const auto& ent : *list) { - if (ent.get() == entity->data.get()) { - count++; - } - } - - return PyLong_FromSsize_t(count); -} - -PyObject* UIEntityCollection::subscript(PyUIEntityCollectionObject* 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; - } - - // Iterate through the list with slice parameters - auto it = self->data->begin(); - for (Py_ssize_t i = 0, cur = start; i < slicelength; i++, cur += step) { - auto cur_it = it; - std::advance(cur_it, cur); - - auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"); - auto obj = (PyUIEntityObject*)type->tp_alloc(type, 0); - if (obj) { - obj->data = *cur_it; - PyList_SET_ITEM(result_list, i, (PyObject*)obj); // Steals reference - } else { - Py_DECREF(result_list); - Py_DECREF(type); - return NULL; - } - Py_DECREF(type); - } - - return result_list; - } else { - PyErr_Format(PyExc_TypeError, "EntityCollection indices must be integers or slices, not %.200s", - Py_TYPE(key)->tp_name); - return NULL; - } -} - -int UIEntityCollection::ass_subscript(PyUIEntityCollectionObject* 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 indices; - for (Py_ssize_t i = 0, cur = start; i < slicelength; i++, cur += step) { - indices.push_back(cur); - } - // Sort in descending order - std::sort(indices.begin(), indices.end(), std::greater()); - - // Delete each index - for (Py_ssize_t idx : indices) { - auto it = self->data->begin(); - std::advance(it, idx); - (*it)->grid = nullptr; // Clear grid reference - self->data->erase(it); - } - } else { - // Contiguous slice - delete range - auto it_start = self->data->begin(); - auto it_stop = self->data->begin(); - std::advance(it_start, start); - std::advance(it_stop, stop); - - // Clear grid references - for (auto it = it_start; it != it_stop; ++it) { - (*it)->grid = nullptr; - } - - self->data->erase(it_start, it_stop); - } - 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> new_items; - for (Py_ssize_t i = 0; i < value_len; i++) { - PyObject* item = PySequence_GetItem(value, i); - if (!item) { - return -1; - } - - // 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 -1; - } - - PyUIEntityObject* entity = (PyUIEntityObject*)item; - Py_DECREF(item); - new_items.push_back(entity->data); - } - - // Now perform the assignment - if (step == 1) { - // Contiguous slice - if (slicelength != value_len) { - // Need to resize - remove old items and insert new ones - auto it_start = self->data->begin(); - auto it_stop = self->data->begin(); - std::advance(it_start, start); - std::advance(it_stop, stop); - - // Clear grid references from old items - for (auto it = it_start; it != it_stop; ++it) { - (*it)->grid = nullptr; - } - - // Erase old range - it_start = self->data->erase(it_start, it_stop); - - // Insert new items - for (const auto& entity : new_items) { - entity->grid = self->grid; - it_start = self->data->insert(it_start, entity); - ++it_start; - } - } else { - // Same size, just replace - auto it = self->data->begin(); - std::advance(it, start); - for (const auto& entity : new_items) { - (*it)->grid = nullptr; // Clear old grid ref - *it = entity; - entity->grid = self->grid; // Set new grid ref - ++it; - } - } - } 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; - } - - auto list_it = self->data->begin(); - for (Py_ssize_t i = 0, cur = start; i < slicelength; i++, cur += step) { - auto cur_it = list_it; - std::advance(cur_it, cur); - (*cur_it)->grid = nullptr; // Clear old grid ref - *cur_it = new_items[i]; - new_items[i]->grid = self->grid; // Set new grid ref - } - } - - return 0; - } - } else { - PyErr_Format(PyExc_TypeError, "EntityCollection indices must be integers or slices, not %.200s", - Py_TYPE(key)->tp_name); - return -1; - } -} - -PyMappingMethods UIEntityCollection::mpmethods = { - .mp_length = (lenfunc)UIEntityCollection::len, - .mp_subscript = (binaryfunc)UIEntityCollection::subscript, - .mp_ass_subscript = (objobjargproc)UIEntityCollection::ass_subscript -}; - -// Helper function for entity name matching with wildcards -static bool matchEntityName(const std::string& name, const std::string& pattern) { - if (pattern.find('*') != std::string::npos) { - if (pattern == "*") { - return true; - } else if (pattern.front() == '*' && pattern.back() == '*' && pattern.length() > 2) { - std::string substring = pattern.substr(1, pattern.length() - 2); - return name.find(substring) != std::string::npos; - } else if (pattern.front() == '*') { - std::string suffix = pattern.substr(1); - return name.length() >= suffix.length() && - name.compare(name.length() - suffix.length(), suffix.length(), suffix) == 0; - } else if (pattern.back() == '*') { - std::string prefix = pattern.substr(0, pattern.length() - 1); - return name.compare(0, prefix.length(), prefix) == 0; - } - return name == pattern; - } - return name == pattern; -} - -PyObject* UIEntityCollection::find(PyUIEntityCollectionObject* self, PyObject* args, PyObject* kwds) { - const char* name = nullptr; - - static const char* kwlist[] = {"name", NULL}; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "s", const_cast(kwlist), &name)) { - return NULL; - } - - auto list = self->data.get(); - if (!list) { - PyErr_SetString(PyExc_RuntimeError, "Collection data is null"); - return NULL; - } - - std::string pattern(name); - bool has_wildcard = (pattern.find('*') != std::string::npos); - - // Get the Entity type for creating Python objects - PyTypeObject* entityType = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"); - if (!entityType) { - PyErr_SetString(PyExc_RuntimeError, "Could not find Entity type"); - return NULL; - } - - if (has_wildcard) { - // Return list of all matches - PyObject* results = PyList_New(0); - if (!results) { - Py_DECREF(entityType); - return NULL; - } - - for (auto& entity : *list) { - // Entity name is stored in sprite.name - if (matchEntityName(entity->sprite.name, pattern)) { - PyUIEntityObject* py_entity = (PyUIEntityObject*)entityType->tp_alloc(entityType, 0); - if (!py_entity) { - Py_DECREF(results); - Py_DECREF(entityType); - return NULL; - } - py_entity->data = entity; - py_entity->weakreflist = NULL; - - if (PyList_Append(results, (PyObject*)py_entity) < 0) { - Py_DECREF(py_entity); - Py_DECREF(results); - Py_DECREF(entityType); - return NULL; - } - Py_DECREF(py_entity); // PyList_Append increfs - } - } - - Py_DECREF(entityType); - return results; - } else { - // Return first exact match or None - for (auto& entity : *list) { - if (entity->sprite.name == pattern) { - PyUIEntityObject* py_entity = (PyUIEntityObject*)entityType->tp_alloc(entityType, 0); - if (!py_entity) { - Py_DECREF(entityType); - return NULL; - } - py_entity->data = entity; - py_entity->weakreflist = NULL; - Py_DECREF(entityType); - return (PyObject*)py_entity; - } - } - - Py_DECREF(entityType); - Py_RETURN_NONE; - } -} - -PyMethodDef UIEntityCollection::methods[] = { - {"append", (PyCFunction)UIEntityCollection::append, METH_O, - "append(entity)\n\n" - "Add an entity to the end of the collection."}, - {"extend", (PyCFunction)UIEntityCollection::extend, METH_O, - "extend(iterable)\n\n" - "Add all entities from an iterable to the collection."}, - {"insert", (PyCFunction)UIEntityCollection::insert, METH_VARARGS, - "insert(index, entity)\n\n" - "Insert entity at index. Like list.insert(), indices past the end append."}, - {"remove", (PyCFunction)UIEntityCollection::remove, METH_O, - "remove(entity)\n\n" - "Remove first occurrence of entity. Raises ValueError if not found."}, - {"pop", (PyCFunction)UIEntityCollection::pop, METH_VARARGS, - "pop([index]) -> entity\n\n" - "Remove and return entity at index (default: last entity)."}, - {"index", (PyCFunction)UIEntityCollection::index_method, METH_O, - "index(entity) -> int\n\n" - "Return index of first occurrence of entity. Raises ValueError if not found."}, - {"count", (PyCFunction)UIEntityCollection::count, METH_O, - "count(entity) -> int\n\n" - "Count occurrences of entity in the collection."}, - {"find", (PyCFunction)UIEntityCollection::find, METH_VARARGS | METH_KEYWORDS, - "find(name) -> entity or list\n\n" - "Find entities by name.\n\n" - "Args:\n" - " name (str): Name to search for. Supports wildcards:\n" - " - 'exact' for exact match (returns single entity or None)\n" - " - 'prefix*' for starts-with match (returns list)\n" - " - '*suffix' for ends-with match (returns list)\n" - " - '*substring*' for contains match (returns list)\n\n" - "Returns:\n" - " Single entity if exact match, list if wildcard, None if not found."}, - {NULL, NULL, 0, NULL} -}; - -PyObject* UIEntityCollection::repr(PyUIEntityCollectionObject* self) -{ - std::ostringstream ss; - if (!self->data) ss << ""; - else { - ss << "data->size() << " child objects)>"; - } - std::string repr_str = ss.str(); - return PyUnicode_DecodeUTF8(repr_str.c_str(), repr_str.size(), "replace"); -} - -int UIEntityCollection::init(PyUIEntityCollectionObject* self, PyObject* args, PyObject* kwds) -{ - PyErr_SetString(PyExc_TypeError, "EntityCollection cannot be instantiated: a C++ data source is required."); - return -1; -} - -PyObject* UIEntityCollection::iter(PyUIEntityCollectionObject* self) -{ - // Use the iterator type directly from namespace (#189 - type not exported to module) - PyTypeObject* iterType = &mcrfpydef::PyUIEntityCollectionIterType; - - // Allocate new iterator instance - PyUIEntityCollectionIterObject* iterObj = (PyUIEntityCollectionIterObject*)iterType->tp_alloc(iterType, 0); - - if (iterObj == NULL) { - return NULL; // Failed to allocate memory for the iterator object - } - - iterObj->data = self->data; - iterObj->current = self->data->begin(); // Start at beginning - iterObj->end = self->data->end(); // Cache end iterator - iterObj->start_size = self->data->size(); - - return (PyObject*)iterObj; -} +// UIEntityCollection code has been moved to UIEntityCollection.cpp // Property system implementation for animations bool UIGrid::setProperty(const std::string& name, float value) { diff --git a/src/UIGrid.h b/src/UIGrid.h index bb707f2..b8a9d2d 100644 --- a/src/UIGrid.h +++ b/src/UIGrid.h @@ -23,6 +23,7 @@ #include "GridLayers.h" #include "GridChunk.h" #include "SpatialHash.h" +#include "UIEntityCollection.h" // EntityCollection types (extracted from UIGrid) class UIGrid: public UIDrawable { @@ -180,6 +181,8 @@ public: static PyMethodDef methods[]; static PyGetSetDef getsetters[]; + static PyMappingMethods mpmethods; // For grid[x, y] subscript access + static PyObject* subscript(PyUIGridObject* self, PyObject* key); // __getitem__ static PyObject* get_entities(PyUIGridObject* self, void* closure); static PyObject* get_children(PyUIGridObject* self, void* closure); static PyObject* repr(PyUIGridObject* self); @@ -200,54 +203,7 @@ public: static PyObject* py_layer(PyUIGridObject* self, PyObject* args); }; -typedef struct { - PyObject_HEAD - std::shared_ptr>> data; - std::shared_ptr grid; -} PyUIEntityCollectionObject; - -class UIEntityCollection { -public: - static PySequenceMethods sqmethods; - static PyMappingMethods mpmethods; - static PyObject* append(PyUIEntityCollectionObject* self, PyObject* o); - static PyObject* extend(PyUIEntityCollectionObject* self, PyObject* o); - static PyObject* remove(PyUIEntityCollectionObject* self, PyObject* o); - static PyObject* pop(PyUIEntityCollectionObject* self, PyObject* args); - static PyObject* insert(PyUIEntityCollectionObject* self, PyObject* args); - static PyObject* index_method(PyUIEntityCollectionObject* self, PyObject* value); - static PyObject* count(PyUIEntityCollectionObject* self, PyObject* value); - static PyObject* find(PyUIEntityCollectionObject* self, PyObject* args, PyObject* kwds); - static PyMethodDef methods[]; - static PyObject* repr(PyUIEntityCollectionObject* self); - static int init(PyUIEntityCollectionObject* self, PyObject* args, PyObject* kwds); - static PyObject* iter(PyUIEntityCollectionObject* self); - static Py_ssize_t len(PyUIEntityCollectionObject* self); - static PyObject* getitem(PyUIEntityCollectionObject* self, Py_ssize_t index); - static int setitem(PyUIEntityCollectionObject* self, Py_ssize_t index, PyObject* value); - static int contains(PyUIEntityCollectionObject* self, PyObject* value); - static PyObject* concat(PyUIEntityCollectionObject* self, PyObject* other); - static PyObject* inplace_concat(PyUIEntityCollectionObject* self, PyObject* other); - static PyObject* subscript(PyUIEntityCollectionObject* self, PyObject* key); - static int ass_subscript(PyUIEntityCollectionObject* self, PyObject* key, PyObject* value); -}; - -typedef struct { - PyObject_HEAD - std::shared_ptr>> data; - std::list>::iterator current; // Actual list iterator - O(1) increment - std::list>::iterator end; // End iterator for bounds check - int start_size; // For detecting modification during iteration -} PyUIEntityCollectionIterObject; - -class UIEntityCollectionIter { -public: - static int init(PyUIEntityCollectionIterObject* self, PyObject* args, PyObject* kwds); - static PyObject* next(PyUIEntityCollectionIterObject* self); - static PyObject* repr(PyUIEntityCollectionIterObject* self); - static PyObject* getitem(PyUIEntityCollectionObject* self, Py_ssize_t index); - -}; +// UIEntityCollection types are now in UIEntityCollection.h // Forward declaration of methods array extern PyMethodDef UIGrid_all_methods[]; @@ -268,11 +224,8 @@ namespace mcrfpydef { obj->data.reset(); Py_TYPE(self)->tp_free(self); }, - //TODO - PyUIGrid REPR def: .tp_repr = (reprfunc)UIGrid::repr, - //.tp_hash = NULL, - //.tp_iter - //.tp_iternext + .tp_as_mapping = &UIGrid::mpmethods, // Enable grid[x, y] subscript access .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, .tp_doc = PyDoc_STR("Grid(pos=None, size=None, grid_size=None, texture=None, **kwargs)\n\n" "A grid-based UI element for tile-based rendering and entity management.\n\n" @@ -330,61 +283,6 @@ namespace mcrfpydef { } }; - // #189 - Use inline instead of static to ensure single instance across translation units - inline PyTypeObject PyUIEntityCollectionIterType = { - .ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0}, - .tp_name = "mcrfpy.UIEntityCollectionIter", - .tp_basicsize = sizeof(PyUIEntityCollectionIterObject), - .tp_itemsize = 0, - .tp_dealloc = (destructor)[](PyObject* self) - { - PyUIEntityCollectionIterObject* obj = (PyUIEntityCollectionIterObject*)self; - obj->data.reset(); - Py_TYPE(self)->tp_free(self); - }, - .tp_repr = (reprfunc)UIEntityCollectionIter::repr, - .tp_flags = Py_TPFLAGS_DEFAULT, - .tp_doc = PyDoc_STR("Iterator for a collection of UI objects"), - .tp_iter = PyObject_SelfIter, - .tp_iternext = (iternextfunc)UIEntityCollectionIter::next, - //.tp_getset = UIEntityCollection::getset, - .tp_init = (initproc)UIEntityCollectionIter::init, // just raise an exception - .tp_alloc = PyType_GenericAlloc, - .tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject* - { - PyErr_SetString(PyExc_TypeError, "UICollection cannot be instantiated: a C++ data source is required."); - return NULL; - } - }; - - // #189 - Use inline instead of static to ensure single instance across translation units - inline PyTypeObject PyUIEntityCollectionType = { - .ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0}, - .tp_name = "mcrfpy.EntityCollection", - .tp_basicsize = sizeof(PyUIEntityCollectionObject), - .tp_itemsize = 0, - .tp_dealloc = (destructor)[](PyObject* self) - { - PyUIEntityCollectionObject* obj = (PyUIEntityCollectionObject*)self; - obj->data.reset(); - Py_TYPE(self)->tp_free(self); - }, - .tp_repr = (reprfunc)UIEntityCollection::repr, - .tp_as_sequence = &UIEntityCollection::sqmethods, - .tp_as_mapping = &UIEntityCollection::mpmethods, - .tp_flags = Py_TPFLAGS_DEFAULT, - .tp_doc = PyDoc_STR("Iterable, indexable collection of Entities"), - .tp_iter = (getiterfunc)UIEntityCollection::iter, - .tp_methods = UIEntityCollection::methods, // append, remove - //.tp_getset = UIEntityCollection::getset, - .tp_init = (initproc)UIEntityCollection::init, // just raise an exception - .tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject* - { - // Does PyUIEntityCollectionType need __new__ if it's not supposed to be instantiable by the user? - // Should I just raise an exception? Or is the uninitialized shared_ptr enough of a blocker? - PyErr_SetString(PyExc_TypeError, "EntityCollection cannot be instantiated: a C++ data source is required."); - return NULL; - } - }; + // EntityCollection types moved to UIEntityCollection.h }