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

@ -6,6 +6,7 @@
#include "PyFOV.h"
#include "PyPositionHelper.h"
#include "PyHeightMap.h"
#include "PythonObjectCache.h"
#include <sstream>
// =============================================================================
@ -1632,17 +1633,31 @@ PyObject* PyGridLayerAPI::ColorLayer_get_grid(PyColorLayerObject* self, void* cl
Py_RETURN_NONE;
}
// Create Python Grid wrapper for the parent grid
auto* grid_type = (PyTypeObject*)PyObject_GetAttrString(
PyImport_ImportModule("mcrfpy"), "Grid");
if (!grid_type) return NULL;
// Check cache first — preserves identity (layer.grid is layer.grid)
if (self->grid->serial_number != 0) {
PyObject* cached = PythonObjectCache::getInstance().lookup(self->grid->serial_number);
if (cached) {
return cached;
}
}
// No cached wrapper — allocate a new one
auto* grid_type = &mcrfpydef::PyUIGridType;
PyUIGridObject* py_grid = (PyUIGridObject*)grid_type->tp_alloc(grid_type, 0);
Py_DECREF(grid_type);
if (!py_grid) return NULL;
py_grid->data = self->grid;
py_grid->weakreflist = NULL;
// Register in cache
if (self->grid->serial_number == 0) {
self->grid->serial_number = PythonObjectCache::getInstance().assignSerial();
}
PyObject* weakref = PyWeakref_NewRef((PyObject*)py_grid, NULL);
if (weakref) {
PythonObjectCache::getInstance().registerObject(self->grid->serial_number, weakref);
Py_DECREF(weakref);
}
return (PyObject*)py_grid;
}
@ -2267,17 +2282,31 @@ PyObject* PyGridLayerAPI::TileLayer_get_grid(PyTileLayerObject* self, void* clos
Py_RETURN_NONE;
}
// Create Python Grid wrapper for the parent grid
auto* grid_type = (PyTypeObject*)PyObject_GetAttrString(
PyImport_ImportModule("mcrfpy"), "Grid");
if (!grid_type) return NULL;
// Check cache first — preserves identity (layer.grid is layer.grid)
if (self->grid->serial_number != 0) {
PyObject* cached = PythonObjectCache::getInstance().lookup(self->grid->serial_number);
if (cached) {
return cached;
}
}
// No cached wrapper — allocate a new one
auto* grid_type = &mcrfpydef::PyUIGridType;
PyUIGridObject* py_grid = (PyUIGridObject*)grid_type->tp_alloc(grid_type, 0);
Py_DECREF(grid_type);
if (!py_grid) return NULL;
py_grid->data = self->grid;
py_grid->weakreflist = NULL;
// Register in cache
if (self->grid->serial_number == 0) {
self->grid->serial_number = PythonObjectCache::getInstance().assignSerial();
}
PyObject* weakref = PyWeakref_NewRef((PyObject*)py_grid, NULL);
if (weakref) {
PythonObjectCache::getInstance().registerObject(self->grid->serial_number, weakref);
Py_DECREF(weakref);
}
return (PyObject*)py_grid;
}
@ -2360,6 +2389,11 @@ int PyGridLayerAPI::TileLayer_set_grid(PyTileLayerObject* self, PyObject* value,
py_grid->data->layers_need_sort = true;
self->grid = py_grid->data;
// Inherit grid texture if TileLayer has none (#254)
if (!self->data->texture) {
self->data->texture = py_grid->data->getTexture();
}
return 0;
}