Split UIGrid.cpp into three files for maintainability, closes #149
UIGrid.cpp (3338 lines) split into: - UIGrid.cpp (1588 lines): core logic, rendering, init, click handling, cell callbacks - UIGridPyMethods.cpp (1104 lines): Python method implementations and method tables - UIGridPyProperties.cpp (597 lines): Python getter/setter implementations and getsetters table All 277 tests pass with no regressions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
edd0028270
commit
a6a0722be6
4 changed files with 1842 additions and 1753 deletions
1756
src/UIGrid.cpp
1756
src/UIGrid.cpp
File diff suppressed because it is too large
Load diff
1104
src/UIGridPyMethods.cpp
Normal file
1104
src/UIGridPyMethods.cpp
Normal file
File diff suppressed because it is too large
Load diff
597
src/UIGridPyProperties.cpp
Normal file
597
src/UIGridPyProperties.cpp
Normal file
|
|
@ -0,0 +1,597 @@
|
||||||
|
// UIGridPyProperties.cpp — Python getter/setter implementations for UIGrid
|
||||||
|
// Extracted from UIGrid.cpp (#149) for maintainability.
|
||||||
|
// Contains: all PyGetSetDef getters/setters and the getsetters[] array.
|
||||||
|
#include "UIGrid.h"
|
||||||
|
#include "UIGridView.h"
|
||||||
|
#include "McRFPy_API.h"
|
||||||
|
#include "PythonObjectCache.h"
|
||||||
|
#include "PyColor.h"
|
||||||
|
#include "PyVector.h"
|
||||||
|
#include "PyFOV.h"
|
||||||
|
#include "UIBase.h"
|
||||||
|
#include "UICollection.h"
|
||||||
|
#include "McRFPy_Doc.h"
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Grid dimension properties
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
PyObject* UIGrid::get_grid_size(PyUIGridObject* self, void* closure) {
|
||||||
|
return PyVector(sf::Vector2f(static_cast<float>(self->data->grid_w),
|
||||||
|
static_cast<float>(self->data->grid_h))).pyObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* UIGrid::get_grid_w(PyUIGridObject* self, void* closure) {
|
||||||
|
return PyLong_FromLong(self->data->grid_w);
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* UIGrid::get_grid_h(PyUIGridObject* self, void* closure) {
|
||||||
|
return PyLong_FromLong(self->data->grid_h);
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* UIGrid::get_size(PyUIGridObject* self, void* closure) {
|
||||||
|
auto& box = self->data->box;
|
||||||
|
return PyVector(box.getSize()).pyObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
int UIGrid::set_size(PyUIGridObject* self, PyObject* value, void* closure) {
|
||||||
|
float w, h;
|
||||||
|
PyVectorObject* vec = PyVector::from_arg(value);
|
||||||
|
if (vec) {
|
||||||
|
w = vec->data.x;
|
||||||
|
h = vec->data.y;
|
||||||
|
Py_DECREF(vec);
|
||||||
|
} else {
|
||||||
|
PyErr_Clear();
|
||||||
|
if (!PyArg_ParseTuple(value, "ff", &w, &h)) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "size must be a Vector or tuple (w, h)");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self->data->box.setSize(sf::Vector2f(w, h));
|
||||||
|
|
||||||
|
unsigned int tex_width = static_cast<unsigned int>(w * 1.5f);
|
||||||
|
unsigned int tex_height = static_cast<unsigned int>(h * 1.5f);
|
||||||
|
|
||||||
|
tex_width = std::min(tex_width, 4096u);
|
||||||
|
tex_height = std::min(tex_height, 4096u);
|
||||||
|
|
||||||
|
self->data->renderTexture.create(tex_width, tex_height);
|
||||||
|
self->data->markDirty();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Camera/view properties
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
PyObject* UIGrid::get_center(PyUIGridObject* self, void* closure) {
|
||||||
|
return PyVector(sf::Vector2f(self->data->center_x, self->data->center_y)).pyObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
int UIGrid::set_center(PyUIGridObject* self, PyObject* value, void* closure) {
|
||||||
|
float x, y;
|
||||||
|
if (!PyArg_ParseTuple(value, "ff", &x, &y)) {
|
||||||
|
PyErr_SetString(PyExc_ValueError, "Size must be a tuple of two floats");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
self->data->center_x = x;
|
||||||
|
self->data->center_y = y;
|
||||||
|
self->data->markDirty();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* UIGrid::get_float_member(PyUIGridObject* self, void* closure)
|
||||||
|
{
|
||||||
|
auto member_ptr = reinterpret_cast<intptr_t>(closure);
|
||||||
|
if (member_ptr == 0)
|
||||||
|
return PyFloat_FromDouble(self->data->box.getPosition().x);
|
||||||
|
else if (member_ptr == 1)
|
||||||
|
return PyFloat_FromDouble(self->data->box.getPosition().y);
|
||||||
|
else if (member_ptr == 2)
|
||||||
|
return PyFloat_FromDouble(self->data->box.getSize().x);
|
||||||
|
else if (member_ptr == 3)
|
||||||
|
return PyFloat_FromDouble(self->data->box.getSize().y);
|
||||||
|
else if (member_ptr == 4)
|
||||||
|
return PyFloat_FromDouble(self->data->center_x);
|
||||||
|
else if (member_ptr == 5)
|
||||||
|
return PyFloat_FromDouble(self->data->center_y);
|
||||||
|
else if (member_ptr == 6)
|
||||||
|
return PyFloat_FromDouble(self->data->zoom);
|
||||||
|
else if (member_ptr == 7)
|
||||||
|
return PyFloat_FromDouble(self->data->camera_rotation);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PyErr_SetString(PyExc_AttributeError, "Invalid attribute");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int UIGrid::set_float_member(PyUIGridObject* self, PyObject* value, void* closure)
|
||||||
|
{
|
||||||
|
float val;
|
||||||
|
auto member_ptr = reinterpret_cast<intptr_t>(closure);
|
||||||
|
if (PyFloat_Check(value))
|
||||||
|
{
|
||||||
|
val = PyFloat_AsDouble(value);
|
||||||
|
}
|
||||||
|
else if (PyLong_Check(value))
|
||||||
|
{
|
||||||
|
val = PyLong_AsLong(value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PyErr_SetString(PyExc_TypeError, "Value must be a number (int or float)");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (member_ptr == 0)
|
||||||
|
self->data->box.setPosition(val, self->data->box.getPosition().y);
|
||||||
|
else if (member_ptr == 1)
|
||||||
|
self->data->box.setPosition(self->data->box.getPosition().x, val);
|
||||||
|
else if (member_ptr == 2)
|
||||||
|
{
|
||||||
|
self->data->box.setSize(sf::Vector2f(val, self->data->box.getSize().y));
|
||||||
|
unsigned int tex_width = static_cast<unsigned int>(val * 1.5f);
|
||||||
|
unsigned int tex_height = static_cast<unsigned int>(self->data->box.getSize().y * 1.5f);
|
||||||
|
tex_width = std::min(tex_width, 4096u);
|
||||||
|
tex_height = std::min(tex_height, 4096u);
|
||||||
|
self->data->renderTexture.create(tex_width, tex_height);
|
||||||
|
}
|
||||||
|
else if (member_ptr == 3)
|
||||||
|
{
|
||||||
|
self->data->box.setSize(sf::Vector2f(self->data->box.getSize().x, val));
|
||||||
|
unsigned int tex_width = static_cast<unsigned int>(self->data->box.getSize().x * 1.5f);
|
||||||
|
unsigned int tex_height = static_cast<unsigned int>(val * 1.5f);
|
||||||
|
tex_width = std::min(tex_width, 4096u);
|
||||||
|
tex_height = std::min(tex_height, 4096u);
|
||||||
|
self->data->renderTexture.create(tex_width, tex_height);
|
||||||
|
}
|
||||||
|
else if (member_ptr == 4)
|
||||||
|
self->data->center_x = val;
|
||||||
|
else if (member_ptr == 5)
|
||||||
|
self->data->center_y = val;
|
||||||
|
else if (member_ptr == 6)
|
||||||
|
self->data->zoom = val;
|
||||||
|
else if (member_ptr == 7)
|
||||||
|
self->data->camera_rotation = val;
|
||||||
|
|
||||||
|
if (self->view) {
|
||||||
|
if (member_ptr == 0)
|
||||||
|
self->view->box.setPosition(val, self->view->box.getPosition().y);
|
||||||
|
else if (member_ptr == 1)
|
||||||
|
self->view->box.setPosition(self->view->box.getPosition().x, val);
|
||||||
|
else if (member_ptr == 2)
|
||||||
|
self->view->box.setSize(sf::Vector2f(val, self->view->box.getSize().y));
|
||||||
|
else if (member_ptr == 3)
|
||||||
|
self->view->box.setSize(sf::Vector2f(self->view->box.getSize().x, val));
|
||||||
|
else if (member_ptr == 4) self->view->center_x = val;
|
||||||
|
else if (member_ptr == 5) self->view->center_y = val;
|
||||||
|
else if (member_ptr == 6) self->view->zoom = val;
|
||||||
|
else if (member_ptr == 7) self->view->camera_rotation = val;
|
||||||
|
self->view->position = self->view->box.getPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (member_ptr == 0 || member_ptr == 1) {
|
||||||
|
self->data->markCompositeDirty();
|
||||||
|
} else {
|
||||||
|
self->data->markDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Texture property
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
PyObject* UIGrid::get_texture(PyUIGridObject* self, void* closure) {
|
||||||
|
auto texture = self->data->getTexture();
|
||||||
|
if (!texture) {
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto type = &mcrfpydef::PyTextureType;
|
||||||
|
auto obj = (PyTextureObject*)type->tp_alloc(type, 0);
|
||||||
|
obj->data = texture;
|
||||||
|
return (PyObject*)obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Fill color
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
PyObject* UIGrid::get_fill_color(PyUIGridObject* self, void* closure)
|
||||||
|
{
|
||||||
|
auto& color = self->data->fill_color;
|
||||||
|
auto type = &mcrfpydef::PyColorType;
|
||||||
|
PyObject* args = Py_BuildValue("(iiii)", color.r, color.g, color.b, color.a);
|
||||||
|
PyObject* obj = PyObject_CallObject((PyObject*)type, args);
|
||||||
|
Py_DECREF(args);
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
int UIGrid::set_fill_color(PyUIGridObject* self, PyObject* value, void* closure)
|
||||||
|
{
|
||||||
|
if (!PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyColorType)) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "fill_color must be a Color object");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyColorObject* color = (PyColorObject*)value;
|
||||||
|
self->data->fill_color = color->data;
|
||||||
|
self->data->markDirty();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Perspective properties
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
PyObject* UIGrid::get_perspective(PyUIGridObject* self, void* closure)
|
||||||
|
{
|
||||||
|
auto locked = self->data->perspective_entity.lock();
|
||||||
|
if (locked) {
|
||||||
|
if (locked->serial_number != 0) {
|
||||||
|
PyObject* cached = PythonObjectCache::getInstance().lookup(locked->serial_number);
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto type = &mcrfpydef::PyUIEntityType;
|
||||||
|
auto o = (PyUIEntityObject*)type->tp_alloc(type, 0);
|
||||||
|
if (o) {
|
||||||
|
o->data = locked;
|
||||||
|
o->weakreflist = NULL;
|
||||||
|
return (PyObject*)o;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
int UIGrid::set_perspective(PyUIGridObject* self, PyObject* value, void* closure)
|
||||||
|
{
|
||||||
|
if (value == Py_None) {
|
||||||
|
self->data->perspective_entity.reset();
|
||||||
|
self->data->markDirty();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIEntityType)) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "perspective must be a UIEntity or None");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyUIEntityObject* entity_obj = (PyUIEntityObject*)value;
|
||||||
|
self->data->perspective_entity = entity_obj->data;
|
||||||
|
self->data->perspective_enabled = true;
|
||||||
|
self->data->markDirty();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* UIGrid::get_perspective_enabled(PyUIGridObject* self, void* closure)
|
||||||
|
{
|
||||||
|
return PyBool_FromLong(self->data->perspective_enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
int UIGrid::set_perspective_enabled(PyUIGridObject* self, PyObject* value, void* closure)
|
||||||
|
{
|
||||||
|
int enabled = PyObject_IsTrue(value);
|
||||||
|
if (enabled == -1) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
self->data->perspective_enabled = enabled;
|
||||||
|
self->data->markDirty();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// FOV properties
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
PyObject* UIGrid::get_fov(PyUIGridObject* self, void* closure)
|
||||||
|
{
|
||||||
|
if (PyFOV::fov_enum_class) {
|
||||||
|
PyObject* value = PyLong_FromLong(self->data->fov_algorithm);
|
||||||
|
if (!value) return NULL;
|
||||||
|
|
||||||
|
PyObject* args = PyTuple_Pack(1, value);
|
||||||
|
Py_DECREF(value);
|
||||||
|
if (!args) return NULL;
|
||||||
|
|
||||||
|
PyObject* result = PyObject_Call(PyFOV::fov_enum_class, args, NULL);
|
||||||
|
Py_DECREF(args);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return PyLong_FromLong(self->data->fov_algorithm);
|
||||||
|
}
|
||||||
|
|
||||||
|
int UIGrid::set_fov(PyUIGridObject* self, PyObject* value, void* closure)
|
||||||
|
{
|
||||||
|
TCOD_fov_algorithm_t algo;
|
||||||
|
if (!PyFOV::from_arg(value, &algo, nullptr)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
self->data->fov_algorithm = algo;
|
||||||
|
self->data->markDirty();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* UIGrid::get_fov_radius(PyUIGridObject* self, void* closure)
|
||||||
|
{
|
||||||
|
return PyLong_FromLong(self->data->fov_radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
int UIGrid::set_fov_radius(PyUIGridObject* self, PyObject* value, void* closure)
|
||||||
|
{
|
||||||
|
if (!PyLong_Check(value)) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "fov_radius must be an integer");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
long radius = PyLong_AsLong(value);
|
||||||
|
if (radius == -1 && PyErr_Occurred()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (radius < 0) {
|
||||||
|
PyErr_SetString(PyExc_ValueError, "fov_radius must be non-negative");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
self->data->fov_radius = (int)radius;
|
||||||
|
self->data->markDirty();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Collection getters
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
PyObject* UIGrid::get_entities(PyUIGridObject* self, void* closure)
|
||||||
|
{
|
||||||
|
PyTypeObject* type = &mcrfpydef::PyUIEntityCollectionType;
|
||||||
|
auto o = (PyUIEntityCollectionObject*)type->tp_alloc(type, 0);
|
||||||
|
if (o) {
|
||||||
|
o->data = self->data->entities;
|
||||||
|
o->grid = self->data;
|
||||||
|
}
|
||||||
|
return (PyObject*)o;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* UIGrid::get_children(PyUIGridObject* self, void* closure)
|
||||||
|
{
|
||||||
|
PyTypeObject* type = &mcrfpydef::PyUICollectionType;
|
||||||
|
auto o = (PyUICollectionObject*)type->tp_alloc(type, 0);
|
||||||
|
if (o) {
|
||||||
|
o->data = self->data->children;
|
||||||
|
o->owner = self->data;
|
||||||
|
}
|
||||||
|
return (PyObject*)o;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* UIGrid::get_view(PyUIGridObject* self, void* closure)
|
||||||
|
{
|
||||||
|
if (!self->view) Py_RETURN_NONE;
|
||||||
|
auto type = &mcrfpydef::PyUIGridViewType;
|
||||||
|
auto obj = (PyUIGridViewObject*)type->tp_alloc(type, 0);
|
||||||
|
if (!obj) return PyErr_NoMemory();
|
||||||
|
obj->data = self->view;
|
||||||
|
obj->weakreflist = NULL;
|
||||||
|
return (PyObject*)obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* UIGrid::get_layers(PyUIGridObject* self, void* closure) {
|
||||||
|
self->data->sortLayers();
|
||||||
|
|
||||||
|
PyObject* tuple = PyTuple_New(self->data->layers.size());
|
||||||
|
if (!tuple) return NULL;
|
||||||
|
|
||||||
|
auto* mcrfpy_module = PyImport_ImportModule("mcrfpy");
|
||||||
|
if (!mcrfpy_module) {
|
||||||
|
Py_DECREF(tuple);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* color_layer_type = (PyTypeObject*)PyObject_GetAttrString(mcrfpy_module, "ColorLayer");
|
||||||
|
auto* tile_layer_type = (PyTypeObject*)PyObject_GetAttrString(mcrfpy_module, "TileLayer");
|
||||||
|
Py_DECREF(mcrfpy_module);
|
||||||
|
|
||||||
|
if (!color_layer_type || !tile_layer_type) {
|
||||||
|
if (color_layer_type) Py_DECREF(color_layer_type);
|
||||||
|
if (tile_layer_type) Py_DECREF(tile_layer_type);
|
||||||
|
Py_DECREF(tuple);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < self->data->layers.size(); ++i) {
|
||||||
|
auto& layer = self->data->layers[i];
|
||||||
|
PyObject* py_layer = nullptr;
|
||||||
|
|
||||||
|
if (layer->type == GridLayerType::Color) {
|
||||||
|
PyColorLayerObject* obj = (PyColorLayerObject*)color_layer_type->tp_alloc(color_layer_type, 0);
|
||||||
|
if (obj) {
|
||||||
|
obj->data = std::static_pointer_cast<ColorLayer>(layer);
|
||||||
|
obj->grid = self->data;
|
||||||
|
py_layer = (PyObject*)obj;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
PyTileLayerObject* obj = (PyTileLayerObject*)tile_layer_type->tp_alloc(tile_layer_type, 0);
|
||||||
|
if (obj) {
|
||||||
|
obj->data = std::static_pointer_cast<TileLayer>(layer);
|
||||||
|
obj->grid = self->data;
|
||||||
|
py_layer = (PyObject*)obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!py_layer) {
|
||||||
|
Py_DECREF(color_layer_type);
|
||||||
|
Py_DECREF(tile_layer_type);
|
||||||
|
Py_DECREF(tuple);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyTuple_SET_ITEM(tuple, i, py_layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
Py_DECREF(color_layer_type);
|
||||||
|
Py_DECREF(tile_layer_type);
|
||||||
|
return tuple;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// repr
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
PyObject* UIGrid::repr(PyUIGridObject* self)
|
||||||
|
{
|
||||||
|
std::ostringstream ss;
|
||||||
|
if (!self->data) ss << "<Grid (invalid internal object)>";
|
||||||
|
else {
|
||||||
|
auto grid = self->data;
|
||||||
|
auto box = grid->box;
|
||||||
|
ss << "<Grid (x=" << box.getPosition().x << ", y=" << box.getPosition().y << ", w=" << box.getSize().x << ", h=" << box.getSize().y << ", " <<
|
||||||
|
"center=(" << grid->center_x << ", " << grid->center_y << "), zoom=" << grid->zoom <<
|
||||||
|
")>";
|
||||||
|
}
|
||||||
|
std::string repr_str = ss.str();
|
||||||
|
return PyUnicode_DecodeUTF8(repr_str.c_str(), repr_str.size(), "replace");
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Cell callback properties
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
PyObject* UIGrid::get_on_cell_enter(PyUIGridObject* self, void* closure) {
|
||||||
|
if (self->data->on_cell_enter_callable) {
|
||||||
|
PyObject* cb = self->data->on_cell_enter_callable->borrow();
|
||||||
|
Py_INCREF(cb);
|
||||||
|
return cb;
|
||||||
|
}
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
int UIGrid::set_on_cell_enter(PyUIGridObject* self, PyObject* value, void* closure) {
|
||||||
|
if (value == Py_None) {
|
||||||
|
self->data->on_cell_enter_callable.reset();
|
||||||
|
} else {
|
||||||
|
self->data->on_cell_enter_callable = std::make_unique<PyCellHoverCallable>(value);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* UIGrid::get_on_cell_exit(PyUIGridObject* self, void* closure) {
|
||||||
|
if (self->data->on_cell_exit_callable) {
|
||||||
|
PyObject* cb = self->data->on_cell_exit_callable->borrow();
|
||||||
|
Py_INCREF(cb);
|
||||||
|
return cb;
|
||||||
|
}
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
int UIGrid::set_on_cell_exit(PyUIGridObject* self, PyObject* value, void* closure) {
|
||||||
|
if (value == Py_None) {
|
||||||
|
self->data->on_cell_exit_callable.reset();
|
||||||
|
} else {
|
||||||
|
self->data->on_cell_exit_callable = std::make_unique<PyCellHoverCallable>(value);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* UIGrid::get_on_cell_click(PyUIGridObject* self, void* closure) {
|
||||||
|
if (self->data->on_cell_click_callable) {
|
||||||
|
PyObject* cb = self->data->on_cell_click_callable->borrow();
|
||||||
|
Py_INCREF(cb);
|
||||||
|
return cb;
|
||||||
|
}
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
int UIGrid::set_on_cell_click(PyUIGridObject* self, PyObject* value, void* closure) {
|
||||||
|
if (value == Py_None) {
|
||||||
|
self->data->on_cell_click_callable.reset();
|
||||||
|
} else {
|
||||||
|
self->data->on_cell_click_callable = std::make_unique<PyClickCallable>(value);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* UIGrid::get_hovered_cell(PyUIGridObject* self, void* closure) {
|
||||||
|
if (self->data->hovered_cell.has_value()) {
|
||||||
|
return Py_BuildValue("(ii)", self->data->hovered_cell->x, self->data->hovered_cell->y);
|
||||||
|
}
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// getsetters[] table
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
typedef PyUIGridObject PyObjectType;
|
||||||
|
|
||||||
|
PyGetSetDef UIGrid::getsetters[] = {
|
||||||
|
|
||||||
|
{"grid_size", (getter)UIGrid::get_grid_size, NULL, "Grid dimensions (grid_w, grid_h)", NULL},
|
||||||
|
{"grid_w", (getter)UIGrid::get_grid_w, NULL, "Grid width in cells", NULL},
|
||||||
|
{"grid_h", (getter)UIGrid::get_grid_h, NULL, "Grid height in cells", NULL},
|
||||||
|
{"pos", (getter)UIDrawable::get_pos, (setter)UIDrawable::set_pos, "Position of the grid as Vector", (void*)PyObjectsEnum::UIGRID},
|
||||||
|
{"grid_pos", (getter)UIDrawable::get_grid_pos, (setter)UIDrawable::set_grid_pos, "Position in parent grid's tile coordinates (only when parent is Grid)", (void*)PyObjectsEnum::UIGRID},
|
||||||
|
{"size", (getter)UIGrid::get_size, (setter)UIGrid::set_size, "Size of the grid as Vector (width, height)", NULL},
|
||||||
|
{"center", (getter)UIGrid::get_center, (setter)UIGrid::set_center, "Grid coordinate at the center of the Grid's view (pan)", NULL},
|
||||||
|
|
||||||
|
{"entities", (getter)UIGrid::get_entities, NULL, "EntityCollection of entities on this grid", NULL},
|
||||||
|
{"children", (getter)UIGrid::get_children, NULL, "UICollection of UIDrawable children (speech bubbles, effects, overlays)", NULL},
|
||||||
|
{"layers", (getter)UIGrid::get_layers, NULL, "List of grid layers (ColorLayer, TileLayer) sorted by z_index", NULL},
|
||||||
|
|
||||||
|
{"x", (getter)UIDrawable::get_float_member, (setter)UIDrawable::set_float_member, "top-left corner X-coordinate", (void*)((intptr_t)PyObjectsEnum::UIGRID << 8 | 0)},
|
||||||
|
{"y", (getter)UIDrawable::get_float_member, (setter)UIDrawable::set_float_member, "top-left corner Y-coordinate", (void*)((intptr_t)PyObjectsEnum::UIGRID << 8 | 1)},
|
||||||
|
{"w", (getter)UIDrawable::get_float_member, (setter)UIDrawable::set_float_member, "visible widget width", (void*)((intptr_t)PyObjectsEnum::UIGRID << 8 | 2)},
|
||||||
|
{"h", (getter)UIDrawable::get_float_member, (setter)UIDrawable::set_float_member, "visible widget height", (void*)((intptr_t)PyObjectsEnum::UIGRID << 8 | 3)},
|
||||||
|
{"center_x", (getter)UIGrid::get_float_member, (setter)UIGrid::set_float_member, "center of the view X-coordinate", (void*)4},
|
||||||
|
{"center_y", (getter)UIGrid::get_float_member, (setter)UIGrid::set_float_member, "center of the view Y-coordinate", (void*)5},
|
||||||
|
{"zoom", (getter)UIGrid::get_float_member, (setter)UIGrid::set_float_member, "zoom factor for displaying the Grid", (void*)6},
|
||||||
|
{"camera_rotation", (getter)UIGrid::get_float_member, (setter)UIGrid::set_float_member, "Rotation of grid contents around camera center (degrees). The grid widget stays axis-aligned; only the view into the world rotates.", (void*)7},
|
||||||
|
|
||||||
|
{"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click,
|
||||||
|
MCRF_PROPERTY(on_click,
|
||||||
|
"Callable executed when object is clicked. "
|
||||||
|
"Function receives (pos: Vector, button: str, action: str)."
|
||||||
|
), (void*)PyObjectsEnum::UIGRID},
|
||||||
|
|
||||||
|
{"texture", (getter)UIGrid::get_texture, NULL, "Texture of the grid", NULL},
|
||||||
|
{"fill_color", (getter)UIGrid::get_fill_color, (setter)UIGrid::set_fill_color,
|
||||||
|
"Background fill color of the grid. Returns a copy; modifying components requires reassignment. "
|
||||||
|
"For animation, use 'fill_color.r', 'fill_color.g', etc.", NULL},
|
||||||
|
{"perspective", (getter)UIGrid::get_perspective, (setter)UIGrid::set_perspective,
|
||||||
|
"Entity whose perspective to use for FOV rendering (None for omniscient view). "
|
||||||
|
"Setting an entity automatically enables perspective mode.", NULL},
|
||||||
|
{"perspective_enabled", (getter)UIGrid::get_perspective_enabled, (setter)UIGrid::set_perspective_enabled,
|
||||||
|
"Whether to use perspective-based FOV rendering. When True with no valid entity, "
|
||||||
|
"all cells appear undiscovered.", NULL},
|
||||||
|
{"fov", (getter)UIGrid::get_fov, (setter)UIGrid::set_fov,
|
||||||
|
"FOV algorithm for this grid (mcrfpy.FOV enum). "
|
||||||
|
"Used by entity.updateVisibility() and layer methods when fov=None.", NULL},
|
||||||
|
{"fov_radius", (getter)UIGrid::get_fov_radius, (setter)UIGrid::set_fov_radius,
|
||||||
|
"Default FOV radius for this grid. Used when radius not specified.", NULL},
|
||||||
|
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int,
|
||||||
|
MCRF_PROPERTY(z_index,
|
||||||
|
"Z-order for rendering (lower values rendered first). "
|
||||||
|
"Automatically triggers scene resort when changed."
|
||||||
|
), (void*)PyObjectsEnum::UIGRID},
|
||||||
|
{"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UIGRID},
|
||||||
|
UIDRAWABLE_GETSETTERS,
|
||||||
|
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UIGRID),
|
||||||
|
UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UIGRID),
|
||||||
|
UIDRAWABLE_ROTATION_GETSETTERS(PyObjectsEnum::UIGRID),
|
||||||
|
{"on_cell_enter", (getter)UIGrid::get_on_cell_enter, (setter)UIGrid::set_on_cell_enter,
|
||||||
|
"Callback when mouse enters a grid cell. Called with (cell_pos: Vector).", NULL},
|
||||||
|
{"on_cell_exit", (getter)UIGrid::get_on_cell_exit, (setter)UIGrid::set_on_cell_exit,
|
||||||
|
"Callback when mouse exits a grid cell. Called with (cell_pos: Vector).", NULL},
|
||||||
|
{"on_cell_click", (getter)UIGrid::get_on_cell_click, (setter)UIGrid::set_on_cell_click,
|
||||||
|
"Callback when a grid cell is clicked. Called with (cell_pos: Vector).", NULL},
|
||||||
|
{"hovered_cell", (getter)UIGrid::get_hovered_cell, NULL,
|
||||||
|
"Currently hovered cell as (x, y) tuple, or None if not hovering.", NULL},
|
||||||
|
UIDRAWABLE_SHADER_GETSETTERS(PyObjectsEnum::UIGRID),
|
||||||
|
{"view", (getter)UIGrid::get_view, NULL,
|
||||||
|
"Auto-created GridView for rendering (read-only). "
|
||||||
|
"When Grid is appended to a scene, this view is what actually renders.", NULL},
|
||||||
|
{NULL}
|
||||||
|
};
|
||||||
138
tools/build_debug_libs.sh
Executable file
138
tools/build_debug_libs.sh
Executable file
|
|
@ -0,0 +1,138 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Build libtcod-headless with sanitizer instrumentation.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# tools/build_debug_libs.sh # Build with debug symbols only
|
||||||
|
# tools/build_debug_libs.sh --asan # Build with AddressSanitizer
|
||||||
|
# tools/build_debug_libs.sh --tsan # Build with ThreadSanitizer
|
||||||
|
# tools/build_debug_libs.sh --clean # Remove build artifacts first
|
||||||
|
#
|
||||||
|
# Output: __lib_debug/libtcod.so (instrumented)
|
||||||
|
#
|
||||||
|
# Why: The pre-built libtcod in __lib/ is uninstrumented. ASan/TSan can
|
||||||
|
# detect bugs in our code that corrupt libtcod's memory, but cannot detect
|
||||||
|
# bugs originating inside libtcod (FOV, pathfinding) that touch our data.
|
||||||
|
# Building libtcod with the same sanitizer flags closes this gap.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
LIBTCOD_DIR="$PROJECT_ROOT/modules/libtcod-headless"
|
||||||
|
OUTPUT_DIR="$PROJECT_ROOT/__lib_debug"
|
||||||
|
JOBS="$(nproc 2>/dev/null || echo 4)"
|
||||||
|
|
||||||
|
MODE="debug"
|
||||||
|
CLEAN=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--asan) MODE="asan" ;;
|
||||||
|
--tsan) MODE="tsan" ;;
|
||||||
|
--clean) CLEAN=1 ;;
|
||||||
|
--help|-h)
|
||||||
|
echo "Usage: $0 [--asan|--tsan] [--clean]"
|
||||||
|
echo ""
|
||||||
|
echo "Flags:"
|
||||||
|
echo " (default) Debug symbols only (-g -O1)"
|
||||||
|
echo " --asan AddressSanitizer + UBSan instrumentation"
|
||||||
|
echo " --tsan ThreadSanitizer instrumentation"
|
||||||
|
echo " --clean Remove build artifacts before building"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown argument: $arg"
|
||||||
|
echo "Run '$0 --help' for usage."
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
BUILD_DIR="$LIBTCOD_DIR/build-debug-$MODE"
|
||||||
|
|
||||||
|
case "$MODE" in
|
||||||
|
debug)
|
||||||
|
SANITIZER_FLAGS=""
|
||||||
|
echo "=== Building libtcod-headless with debug symbols ==="
|
||||||
|
;;
|
||||||
|
asan)
|
||||||
|
SANITIZER_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer"
|
||||||
|
echo "=== Building libtcod-headless with ASan + UBSan ==="
|
||||||
|
;;
|
||||||
|
tsan)
|
||||||
|
SANITIZER_FLAGS="-fsanitize=thread"
|
||||||
|
echo "=== Building libtcod-headless with TSan ==="
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "$CLEAN" -eq 1 ]; then
|
||||||
|
echo "Cleaning $BUILD_DIR..."
|
||||||
|
rm -rf "$BUILD_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$LIBTCOD_DIR/CMakeLists.txt" ]; then
|
||||||
|
echo "ERROR: libtcod-headless not found at $LIBTCOD_DIR/CMakeLists.txt"
|
||||||
|
echo "Make sure the submodule is initialized:"
|
||||||
|
echo " git submodule update --init modules/libtcod-headless"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$BUILD_DIR"
|
||||||
|
cd "$BUILD_DIR"
|
||||||
|
|
||||||
|
CMAKE_EXTRA_FLAGS=""
|
||||||
|
if [ -n "$SANITIZER_FLAGS" ]; then
|
||||||
|
CMAKE_EXTRA_FLAGS="-DCMAKE_C_FLAGS=$SANITIZER_FLAGS -DCMAKE_CXX_FLAGS=$SANITIZER_FLAGS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f Makefile ]; then
|
||||||
|
echo "Configuring libtcod-headless ($MODE)..."
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
cmake "$LIBTCOD_DIR" \
|
||||||
|
-DCMAKE_BUILD_TYPE=Debug \
|
||||||
|
-DBUILD_SHARED_LIBS=ON \
|
||||||
|
$CMAKE_EXTRA_FLAGS
|
||||||
|
echo "Configuration complete."
|
||||||
|
else
|
||||||
|
echo "Makefile exists, skipping configure (use --clean to reconfigure)."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Building libtcod-headless (-j$JOBS)..."
|
||||||
|
make -j"$JOBS"
|
||||||
|
|
||||||
|
BUILT_LIB=""
|
||||||
|
for candidate in "$BUILD_DIR/libtcod.so" "$BUILD_DIR/libtcod"*.so*; do
|
||||||
|
if [ -f "$candidate" ] && [ ! -L "$candidate" ]; then
|
||||||
|
BUILT_LIB="$candidate"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$BUILT_LIB" ]; then
|
||||||
|
echo "ERROR: Could not find built libtcod.so in $BUILD_DIR"
|
||||||
|
echo "Contents:"
|
||||||
|
ls -la "$BUILD_DIR"/libtcod* 2>/dev/null || echo " (no libtcod files found)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$OUTPUT_DIR"
|
||||||
|
|
||||||
|
BASENAME="$(basename "$BUILT_LIB")"
|
||||||
|
echo "Copying $BUILT_LIB -> $OUTPUT_DIR/$BASENAME"
|
||||||
|
cp "$BUILT_LIB" "$OUTPUT_DIR/$BASENAME"
|
||||||
|
|
||||||
|
cd "$OUTPUT_DIR"
|
||||||
|
for link_name in libtcod.so libtcod.so.2 libtcod.so.2.2; do
|
||||||
|
if [ "$link_name" != "$BASENAME" ]; then
|
||||||
|
ln -sf "$BASENAME" "$link_name"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Instrumented libtcod build complete ==="
|
||||||
|
echo " Mode: $MODE"
|
||||||
|
echo " Library: $OUTPUT_DIR/$BASENAME"
|
||||||
|
echo " Size: $(du -h "$OUTPUT_DIR/$BASENAME" | cut -f1)"
|
||||||
|
echo ""
|
||||||
|
echo "The existing 'make asan' / 'make tsan' targets link __lib_debug/ first,"
|
||||||
|
echo "so the instrumented libtcod will be picked up automatically."
|
||||||
Loading…
Add table
Add a link
Reference in a new issue