Phase 1: Safety & performance foundation for Grid/Entity overhaul

- Fix Entity3D self-reference cycle: replace raw `self` pointer with
  `pyobject` strong-ref pattern matching UIEntity (closes #266)
- TileLayer inherits Grid texture when none set, in all three attachment
  paths: constructor, add_layer(), and .grid property (closes #254)
- Add SpatialHash::queryCell() for O(1) entity-at-cell lookup; fix
  missing spatial_hash.insert() in Entity.__init__ grid= kwarg path;
  use queryCell in GridPoint.entities (closes #253)
- Add FOV dirty flag and parameter cache to skip redundant computeFOV
  calls when map unchanged and params match (closes #292)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
John McCardle 2026-03-15 21:48:24 -04:00
commit 94f5f5a3fd
13 changed files with 436 additions and 47 deletions

View file

@ -281,7 +281,9 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
self->data->grid = pygrid->data;
// Append entity to grid's entity list
pygrid->data->entities->push_back(self->data);
// Insert into spatial hash for O(1) cell queries (#253)
pygrid->data->spatial_hash.insert(self->data);
// Don't initialize gridstate here - lazy initialization to support large numbers of entities
// gridstate will be initialized when visibility is updated or accessed
}
@ -634,14 +636,33 @@ PyObject* UIEntity::get_grid(PyUIEntityObject* self, void* closure)
Py_RETURN_NONE;
}
// Return a Python Grid object wrapping the C++ grid
auto grid_type = &mcrfpydef::PyUIGridType;
auto& grid = self->data->grid;
// Check cache first — preserves identity (entity.grid is entity.grid)
if (grid->serial_number != 0) {
PyObject* cached = PythonObjectCache::getInstance().lookup(grid->serial_number);
if (cached) {
return cached; // Already INCREF'd by lookup
}
}
// No cached wrapper — allocate a new one
auto grid_type = &mcrfpydef::PyUIGridType;
auto pyGrid = (PyUIGridObject*)grid_type->tp_alloc(grid_type, 0);
if (pyGrid) {
pyGrid->data = self->data->grid;
pyGrid->data = grid;
pyGrid->weakreflist = NULL;
// Register in cache so future accesses return the same wrapper
if (grid->serial_number == 0) {
grid->serial_number = PythonObjectCache::getInstance().assignSerial();
}
PyObject* weakref = PyWeakref_NewRef((PyObject*)pyGrid, NULL);
if (weakref) {
PythonObjectCache::getInstance().registerObject(grid->serial_number, weakref);
Py_DECREF(weakref);
}
}
return (PyObject*)pyGrid;
}