Cell logic storage was an array-of-UIGridPoint: 24 bytes/cell for 2 bytes of real payload (walkable/transparent bools), the other 22 being redundant grid_x/ grid_y and a parent_grid back-pointer. Grids larger than 64 in either dimension switched to non-contiguous 64x64 chunk storage, so no whole-grid contiguous view was possible at any size. Replaced with two dense row-major std::vector<uint8_t> planes on GridData (walkable_plane, transparent_plane), indexed y*grid_w + x, with inline is/setWalkable / is/setTransparent accessors. Chunking is dropped for LOGIC data entirely (render caches still chunk independently in GridLayers) -- so cells are contiguous at any size. Result: 12x smaller logic layer (2 vs 24 B/cell) and cache-friendly iteration; a 1024x1024 grid's logic data is 2 MB instead of 24 MB. The Python GridPoint wrapper already held (grid, x, y) and re-resolved per access, so it just calls the new accessors -- low-invasiveness. GridData::at() -> UIGridPoint& is removed; its callers convert to accessors: - UIGridPoint get/set_bool_member + repr (the wrapper) - UIGridPyMethods apply-threshold / apply-ranges heightmap writers - syncTCODMap / syncTCODMapCell (read planes) - EntityBehavior::isCellWalkable and PathProvider::cellWalkable (pathfinding -- both were missed by the first scout; found via exhaustive grep) - UITestScene UIGridPoint's per-cell data members + ctor removed (class now only namespaces the Python type's accessors). GridChunk.h/.cpp and ChunkManager deleted (dead after logic-chunking removal; nothing else referenced them). No walkable/ transparent serialization existed, so no format impact. Unblocks #333 (TCOD map dedup) and a future contiguous numpy view of walkable/transparent. Regression test issue_332_soa_storage_test.py: round-trips walkable/transparent on a 200x160 grid at and across the old 64-cell chunk boundary, default values, toggle-off, subscript vs at() agreement, grid_pos/entities, and a per-cell footprint guard. Suite 311/311 (FOV + pathfinding + heightmap unchanged). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
247 lines
9.4 KiB
C++
247 lines
9.4 KiB
C++
#include "UIGridPoint.h"
|
|
#include "UIGrid.h"
|
|
#include "UIEntity.h" // #114 - for GridPoint.entities
|
|
#include "GridLayers.h" // #150 - for GridLayerType, ColorLayer, TileLayer
|
|
#include "McRFPy_Doc.h" // #177 - for MCRF_PROPERTY macro
|
|
#include <cstring> // #150 - for strcmp
|
|
|
|
// #332 - cell storage moved to GridData's dense uint8 planes; UIGridPoint no
|
|
// longer holds per-cell data. The Python GridPoint wrapper reaches walkable/
|
|
// transparent through the grid's plane accessors keyed by (x, y).
|
|
|
|
// Utility function to convert sf::Color to PyObject*
|
|
PyObject* sfColor_to_PyObject(sf::Color color) {
|
|
// For now, keep returning tuples to avoid breaking existing code
|
|
return Py_BuildValue("(iiii)", color.r, color.g, color.b, color.a);
|
|
}
|
|
|
|
// Utility function to convert PyObject* to sf::Color
|
|
sf::Color PyObject_to_sfColor(PyObject* obj) {
|
|
// Check if it's a mcrfpy.Color object
|
|
int is_color = PyObject_IsInstance(obj, (PyObject*)&mcrfpydef::PyColorType);
|
|
|
|
if (is_color == 1) {
|
|
PyColorObject* color_obj = (PyColorObject*)obj;
|
|
return color_obj->data;
|
|
} else if (is_color == -1) {
|
|
// Error occurred in PyObject_IsInstance
|
|
return sf::Color();
|
|
}
|
|
|
|
// Otherwise try to parse as tuple
|
|
int r, g, b, a = 255; // Default alpha to fully opaque if not specified
|
|
if (!PyArg_ParseTuple(obj, "iii|i", &r, &g, &b, &a)) {
|
|
PyErr_Clear(); // Clear the error from failed tuple parsing
|
|
PyErr_SetString(PyExc_TypeError, "color must be a Color object or a tuple of (r, g, b[, a])");
|
|
return sf::Color(); // Return default color on parse error
|
|
}
|
|
return sf::Color(r, g, b, a);
|
|
}
|
|
|
|
// #150 - Removed get_color/set_color - now handled by layers
|
|
|
|
// #332 - validate the wrapper's (grid, x, y) still name a live in-bounds cell.
|
|
static bool gridPointValid(PyUIGridPointObject* self) {
|
|
return self->grid &&
|
|
self->x >= 0 && self->x < self->grid->grid_w &&
|
|
self->y >= 0 && self->y < self->grid->grid_h;
|
|
}
|
|
|
|
PyObject* UIGridPoint::get_bool_member(PyUIGridPointObject* self, void* closure) {
|
|
if (!gridPointValid(self)) {
|
|
PyErr_SetString(PyExc_RuntimeError, "GridPoint data is no longer valid");
|
|
return NULL;
|
|
}
|
|
if (reinterpret_cast<intptr_t>(closure) == 0) { // walkable
|
|
return PyBool_FromLong(self->grid->isWalkable(self->x, self->y));
|
|
} else { // transparent
|
|
return PyBool_FromLong(self->grid->isTransparent(self->x, self->y));
|
|
}
|
|
}
|
|
|
|
int UIGridPoint::set_bool_member(PyUIGridPointObject* self, PyObject* value, void* closure) {
|
|
if (!gridPointValid(self)) {
|
|
PyErr_SetString(PyExc_RuntimeError, "GridPoint data is no longer valid");
|
|
return -1;
|
|
}
|
|
bool bv;
|
|
if (value == Py_True) bv = true;
|
|
else if (value == Py_False) bv = false;
|
|
else {
|
|
PyErr_SetString(PyExc_ValueError, "Expected a boolean value");
|
|
return -1;
|
|
}
|
|
|
|
if (reinterpret_cast<intptr_t>(closure) == 0) { // walkable
|
|
self->grid->setWalkable(self->x, self->y, bv);
|
|
} else { // transparent
|
|
self->grid->setTransparent(self->x, self->y, bv);
|
|
}
|
|
|
|
// Per-cell TCOD sync (unchanged behavior; #333 will make this lazy)
|
|
self->grid->syncTCODMapCell(self->x, self->y);
|
|
return 0;
|
|
}
|
|
|
|
// #150 - Removed get_int_member/set_int_member - now handled by layers
|
|
|
|
// #114, #253 - Get list of entities at this grid cell (uses spatial hash for O(1) lookup)
|
|
PyObject* UIGridPoint::get_entities(PyUIGridPointObject* self, void* closure) {
|
|
if (!self->grid) {
|
|
PyErr_SetString(PyExc_RuntimeError, "GridPoint has no parent grid");
|
|
return NULL;
|
|
}
|
|
|
|
int target_x = self->x;
|
|
int target_y = self->y;
|
|
|
|
PyObject* list = PyList_New(0);
|
|
if (!list) return NULL;
|
|
|
|
// Use spatial hash for O(bucket_size) lookup instead of O(n) iteration
|
|
auto entities = self->grid->spatial_hash.queryCell(target_x, target_y);
|
|
for (auto& entity : entities) {
|
|
// Create Python Entity object for this entity
|
|
auto type = &mcrfpydef::PyUIEntityType;
|
|
auto obj = (PyUIEntityObject*)type->tp_alloc(type, 0);
|
|
if (!obj) {
|
|
Py_DECREF(list);
|
|
return NULL;
|
|
}
|
|
obj->data = entity;
|
|
if (PyList_Append(list, (PyObject*)obj) < 0) {
|
|
Py_DECREF(obj);
|
|
Py_DECREF(list);
|
|
return NULL;
|
|
}
|
|
Py_DECREF(obj); // List now owns the reference
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
// #177 - Get grid position as tuple
|
|
PyObject* UIGridPoint::get_grid_pos(PyUIGridPointObject* self, void* closure) {
|
|
return Py_BuildValue("(ii)", self->x, self->y);
|
|
}
|
|
|
|
PyGetSetDef UIGridPoint::getsetters[] = {
|
|
{"walkable", (getter)UIGridPoint::get_bool_member, (setter)UIGridPoint::set_bool_member,
|
|
MCRF_PROPERTY(walkable, "Whether this cell can be walked through (bool). Affects pathfinding and TCOD map sync."), (void*)0},
|
|
{"transparent", (getter)UIGridPoint::get_bool_member, (setter)UIGridPoint::set_bool_member,
|
|
MCRF_PROPERTY(transparent, "Whether this cell allows line-of-sight to pass through (bool). Affects FOV computation and TCOD map sync."), (void*)1},
|
|
{"entities", (getter)UIGridPoint::get_entities, NULL,
|
|
MCRF_PROPERTY(entities, "List of Entity objects currently occupying this cell (list, read-only)."), NULL},
|
|
{"grid_pos", (getter)UIGridPoint::get_grid_pos, NULL,
|
|
MCRF_PROPERTY(grid_pos, "Grid coordinates as an (x, y) position (tuple, read-only)."), NULL},
|
|
{NULL} /* Sentinel */
|
|
};
|
|
|
|
PyObject* UIGridPoint::repr(PyUIGridPointObject* self) {
|
|
std::ostringstream ss;
|
|
if (!gridPointValid(self)) ss << "<GridPoint (invalid internal object)>";
|
|
else {
|
|
ss << "<GridPoint (walkable=" << (self->grid->isWalkable(self->x, self->y) ? "True" : "False")
|
|
<< ", transparent=" << (self->grid->isTransparent(self->x, self->y) ? "True" : "False")
|
|
<< ") at (" << self->x << ", " << self->y << ")>";
|
|
}
|
|
std::string repr_str = ss.str();
|
|
return PyUnicode_DecodeUTF8(repr_str.c_str(), repr_str.size(), "replace");
|
|
}
|
|
|
|
// UIGridPointState removed in #294. Per-entity visibility memory now lives on
|
|
// UIEntity::perspective_map as a DiscreteMap. See mcrfpy.Perspective enum.
|
|
|
|
// #150 - Dynamic attribute access for named layers
|
|
PyObject* UIGridPoint::getattro(PyUIGridPointObject* self, PyObject* name) {
|
|
// First try standard attribute lookup (built-in properties)
|
|
PyObject* result = PyObject_GenericGetAttr((PyObject*)self, name);
|
|
if (result != nullptr || !PyErr_ExceptionMatches(PyExc_AttributeError)) {
|
|
return result;
|
|
}
|
|
|
|
// Clear the AttributeError and check for layer name
|
|
PyErr_Clear();
|
|
|
|
if (!self->grid) {
|
|
PyErr_SetString(PyExc_RuntimeError, "GridPoint has no parent grid");
|
|
return nullptr;
|
|
}
|
|
|
|
const char* attr_name = PyUnicode_AsUTF8(name);
|
|
if (!attr_name) return nullptr;
|
|
|
|
// Look up layer by name
|
|
auto layer = self->grid->getLayerByName(attr_name);
|
|
if (!layer) {
|
|
PyErr_Format(PyExc_AttributeError, "'GridPoint' object has no attribute '%s'", attr_name);
|
|
return nullptr;
|
|
}
|
|
|
|
int x = self->x;
|
|
int y = self->y;
|
|
|
|
// Get value based on layer type
|
|
if (layer->type == GridLayerType::Color) {
|
|
auto color_layer = std::static_pointer_cast<ColorLayer>(layer);
|
|
return sfColor_to_PyObject(color_layer->at(x, y));
|
|
} else if (layer->type == GridLayerType::Tile) {
|
|
auto tile_layer = std::static_pointer_cast<TileLayer>(layer);
|
|
return PyLong_FromLong(tile_layer->at(x, y));
|
|
}
|
|
|
|
PyErr_SetString(PyExc_RuntimeError, "Unknown layer type");
|
|
return nullptr;
|
|
}
|
|
|
|
int UIGridPoint::setattro(PyUIGridPointObject* self, PyObject* name, PyObject* value) {
|
|
// First try standard attribute setting (built-in properties)
|
|
// We need to check if this is a known attribute first
|
|
const char* attr_name = PyUnicode_AsUTF8(name);
|
|
if (!attr_name) return -1;
|
|
|
|
// Check if it's a built-in property (defined in getsetters)
|
|
for (PyGetSetDef* gsd = UIGridPoint::getsetters; gsd->name != nullptr; gsd++) {
|
|
if (strcmp(gsd->name, attr_name) == 0) {
|
|
// It's a built-in property, use standard setter
|
|
return PyObject_GenericSetAttr((PyObject*)self, name, value);
|
|
}
|
|
}
|
|
|
|
// Not a built-in property - try layer lookup
|
|
if (!self->grid) {
|
|
PyErr_SetString(PyExc_RuntimeError, "GridPoint has no parent grid");
|
|
return -1;
|
|
}
|
|
|
|
auto layer = self->grid->getLayerByName(attr_name);
|
|
if (!layer) {
|
|
PyErr_Format(PyExc_AttributeError, "'GridPoint' object has no attribute '%s'", attr_name);
|
|
return -1;
|
|
}
|
|
|
|
int x = self->x;
|
|
int y = self->y;
|
|
|
|
// Set value based on layer type
|
|
if (layer->type == GridLayerType::Color) {
|
|
auto color_layer = std::static_pointer_cast<ColorLayer>(layer);
|
|
sf::Color color = PyObject_to_sfColor(value);
|
|
if (PyErr_Occurred()) return -1;
|
|
color_layer->at(x, y) = color;
|
|
color_layer->markDirty(x, y); // Mark only the affected chunk
|
|
return 0;
|
|
} else if (layer->type == GridLayerType::Tile) {
|
|
auto tile_layer = std::static_pointer_cast<TileLayer>(layer);
|
|
if (!PyLong_Check(value)) {
|
|
PyErr_SetString(PyExc_TypeError, "Tile layer values must be integers");
|
|
return -1;
|
|
}
|
|
tile_layer->at(x, y) = PyLong_AsLong(value);
|
|
tile_layer->markDirty(x, y); // Mark only the affected chunk
|
|
return 0;
|
|
}
|
|
|
|
PyErr_SetString(PyExc_RuntimeError, "Unknown layer type");
|
|
return -1;
|
|
}
|