Preserve Python subclass identity for entities in grids (reopens #266)
The Phase 3 fix for #266 removed UIEntity::self which prevented tp_dealloc from ever running. However, this also allowed Python subclass wrappers (GameEntity, ZoneExit, etc.) to be GC'd while the C++ entity lived on in a grid. Later access via grid.entities returned a base Entity wrapper, losing all subclass methods. Fix: Add UIEntity::pyobject field that holds a strong reference to the Python wrapper. Set in init(), cleared when the entity leaves a grid (die(), set_grid(None), collection removal). This keeps subclass identity alive while in a grid, but allows proper GC when the entity is removed. Added releasePyIdentity() helper called at all grid exit points. Regression test exercises Liber Noster patterns: subclass hierarchy, isinstance() checks, combat mixins, tooltip/send methods, GC survival, die(), pop(), remove(), and stress test with 20 entities. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
34c84ce50a
commit
836a0584df
4 changed files with 291 additions and 2 deletions
|
|
@ -156,6 +156,7 @@ int UIEntityCollection::setitem(PyUIEntityCollectionObject* self, Py_ssize_t ind
|
|||
if (self->grid) {
|
||||
self->grid->spatial_hash.remove(*it);
|
||||
}
|
||||
(*it)->releasePyIdentity();
|
||||
(*it)->grid = nullptr;
|
||||
list->erase(it);
|
||||
return 0;
|
||||
|
|
@ -181,6 +182,7 @@ int UIEntityCollection::setitem(PyUIEntityCollectionObject* self, Py_ssize_t ind
|
|||
}
|
||||
|
||||
// Clear grid reference from the old entity
|
||||
(*it)->releasePyIdentity();
|
||||
(*it)->grid = nullptr;
|
||||
|
||||
// Replace the element and set grid reference
|
||||
|
|
@ -409,6 +411,7 @@ int UIEntityCollection::ass_subscript(PyUIEntityCollectionObject* self, PyObject
|
|||
if (self->grid) {
|
||||
self->grid->spatial_hash.remove(*it);
|
||||
}
|
||||
(*it)->releasePyIdentity();
|
||||
(*it)->grid = nullptr;
|
||||
}
|
||||
self->data->erase(start_it, stop_it);
|
||||
|
|
@ -426,6 +429,7 @@ int UIEntityCollection::ass_subscript(PyUIEntityCollectionObject* self, PyObject
|
|||
if (self->grid) {
|
||||
self->grid->spatial_hash.remove(*it);
|
||||
}
|
||||
(*it)->releasePyIdentity();
|
||||
(*it)->grid = nullptr;
|
||||
self->data->erase(it);
|
||||
}
|
||||
|
|
@ -479,6 +483,7 @@ int UIEntityCollection::ass_subscript(PyUIEntityCollectionObject* self, PyObject
|
|||
if (self->grid) {
|
||||
self->grid->spatial_hash.remove(*it);
|
||||
}
|
||||
(*it)->releasePyIdentity();
|
||||
(*it)->grid = nullptr;
|
||||
}
|
||||
|
||||
|
|
@ -610,6 +615,7 @@ PyObject* UIEntityCollection::remove(PyUIEntityCollectionObject* self, PyObject*
|
|||
if (self->grid) {
|
||||
self->grid->spatial_hash.remove(*it);
|
||||
}
|
||||
(*it)->releasePyIdentity();
|
||||
(*it)->grid = nullptr;
|
||||
list->erase(it);
|
||||
Py_RETURN_NONE;
|
||||
|
|
@ -727,6 +733,20 @@ PyObject* UIEntityCollection::pop(PyUIEntityCollectionObject* self, PyObject* ar
|
|||
entity->grid = nullptr;
|
||||
list->erase(it);
|
||||
|
||||
// Return cached Python object if available (preserves subclass identity)
|
||||
if (entity->serial_number != 0) {
|
||||
PyObject* cached = PythonObjectCache::getInstance().lookup(entity->serial_number);
|
||||
if (cached) {
|
||||
// Release identity ref — entity is leaving the grid
|
||||
// The caller now holds a strong ref via 'cached'
|
||||
entity->releasePyIdentity();
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
// Release identity ref (no cached object to return)
|
||||
entity->releasePyIdentity();
|
||||
|
||||
// Create Python object for the entity
|
||||
PyTypeObject* entity_type = &mcrfpydef::PyUIEntityType;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue