Fix gridstate heap overflows and spatial hash cleanup

Add ensureGridstate() helper that unconditionally checks gridstate size
against current grid dimensions and resizes if mismatched. Replace all
lazy-init guards (size == 0) with ensureGridstate() calls.

Previously, gridstate was only initialized when empty. When an entity
moved to a differently-sized grid, gridstate kept the old size, causing
heap buffer overflows when updateVisibility() or at() iterated using the
new grid's dimensions.

Also adds spatial_hash.remove() calls in set_grid() before removing
entities from old grids, and replaces PyObject_GetAttrString type lookup
with direct &mcrfpydef::PyUIGridType reference.

Closes #258, closes #259, closes #260, closes #261, closes #262,
closes #263, closes #274, closes #276, closes #278

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
John McCardle 2026-03-07 22:56:16 -05:00
commit 348826a0f5
5 changed files with 203 additions and 130 deletions

View file

@ -33,19 +33,24 @@ UIEntity::~UIEntity() {
// Removed UIEntity(UIGrid&) constructor - using lazy initialization instead
void UIEntity::updateVisibility()
void UIEntity::ensureGridstate()
{
if (!grid) return;
// Lazy initialize gridstate if needed
if (gridstate.size() == 0) {
gridstate.resize(grid->grid_w * grid->grid_h);
// Initialize all cells as not visible/discovered
size_t expected = static_cast<size_t>(grid->grid_w) * grid->grid_h;
if (gridstate.size() != expected) {
gridstate.resize(expected);
for (auto& state : gridstate) {
state.visible = false;
state.discovered = false;
}
}
}
void UIEntity::updateVisibility()
{
if (!grid) return;
ensureGridstate();
// First, mark all cells as not visible
for (auto& state : gridstate) {
@ -108,15 +113,7 @@ PyObject* UIEntity::at(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
return NULL;
}
// Lazy initialize gridstate if needed
if (self->data->gridstate.size() == 0) {
self->data->gridstate.resize(self->data->grid->grid_w * self->data->grid->grid_h);
// Initialize all cells as not visible/discovered
for (auto& state : self->data->gridstate) {
state.visible = false;
state.discovered = false;
}
}
self->data->ensureGridstate();
// Bounds check
if (x < 0 || x >= self->data->grid->grid_w || y < 0 || y >= self->data->grid->grid_h) {
@ -662,6 +659,8 @@ int UIEntity::set_grid(PyUIEntityObject* self, PyObject* value, void* closure)
// Handle None - remove from current grid
if (value == Py_None) {
if (self->data->grid) {
// Remove from spatial hash before removing from entity list
self->data->grid->spatial_hash.remove(self->data);
// Remove from current grid's entity list
auto& entities = self->data->grid->entities;
auto it = std::find_if(entities->begin(), entities->end(),
@ -677,11 +676,7 @@ int UIEntity::set_grid(PyUIEntityObject* self, PyObject* value, void* closure)
}
// Value must be a Grid
PyTypeObject* grid_type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid");
bool is_grid = grid_type && PyObject_IsInstance(value, (PyObject*)grid_type);
Py_XDECREF(grid_type);
if (!is_grid) {
if (!PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridType)) {
PyErr_SetString(PyExc_TypeError, "grid must be a Grid or None");
return -1;
}
@ -690,6 +685,7 @@ int UIEntity::set_grid(PyUIEntityObject* self, PyObject* value, void* closure)
// Remove from old grid first (if any)
if (self->data->grid && self->data->grid != new_grid) {
self->data->grid->spatial_hash.remove(self->data);
auto& old_entities = self->data->grid->entities;
auto it = std::find_if(old_entities->begin(), old_entities->end(),
[self](const std::shared_ptr<UIEntity>& e) {
@ -705,14 +701,8 @@ int UIEntity::set_grid(PyUIEntityObject* self, PyObject* value, void* closure)
new_grid->entities->push_back(self->data);
self->data->grid = new_grid;
// Initialize gridstate if needed
if (self->data->gridstate.size() == 0) {
self->data->gridstate.resize(new_grid->grid_w * new_grid->grid_h);
for (auto& state : self->data->gridstate) {
state.visible = false;
state.discovered = false;
}
}
// Resize gridstate to match new grid dimensions
self->data->ensureGridstate();
}
return 0;

View file

@ -73,6 +73,7 @@ public:
~UIEntity();
// Visibility methods
void ensureGridstate(); // Resize gridstate to match current grid dimensions
void updateVisibility(); // Update gridstate from current FOV
// Property system for animations

View file

@ -190,6 +190,7 @@ int UIEntityCollection::setitem(PyUIEntityCollectionObject* self, Py_ssize_t ind
// Replace the element and set grid reference
*it = entity->data;
entity->data->grid = self->grid;
entity->data->ensureGridstate();
// Add to spatial hash
if (self->grid) {
@ -492,6 +493,7 @@ int UIEntityCollection::ass_subscript(PyUIEntityCollectionObject* self, PyObject
for (const auto& entity : new_items) {
self->data->insert(insert_pos, entity);
entity->grid = self->grid;
entity->ensureGridstate();
if (self->grid) {
self->grid->spatial_hash.insert(entity);
}
@ -518,6 +520,7 @@ int UIEntityCollection::ass_subscript(PyUIEntityCollectionObject* self, PyObject
*cur_it = new_items[new_idx++];
(*cur_it)->grid = self->grid;
(*cur_it)->ensureGridstate();
if (self->grid) {
self->grid->spatial_hash.insert(*cur_it);
@ -578,14 +581,8 @@ PyObject* UIEntityCollection::append(PyUIEntityCollectionObject* self, PyObject*
}
}
// 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;
}
}
// Ensure gridstate matches current grid dimensions
entity->data->ensureGridstate();
Py_RETURN_NONE;
}
@ -683,14 +680,8 @@ PyObject* UIEntityCollection::extend(PyUIEntityCollectionObject* self, PyObject*
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;
}
}
// Ensure gridstate matches current grid dimensions
entity->data->ensureGridstate();
Py_DECREF(entity); // Release the reference we held during validation
}
@ -803,14 +794,8 @@ PyObject* UIEntityCollection::insert(PyUIEntityCollectionObject* self, PyObject*
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;
}
}
// Ensure gridstate matches current grid dimensions
entity->data->ensureGridstate();
Py_RETURN_NONE;
}