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:
John McCardle 2026-04-10 04:08:27 -04:00
commit a6a0722be6
4 changed files with 1842 additions and 1753 deletions

File diff suppressed because it is too large Load diff

1104
src/UIGridPyMethods.cpp Normal file

File diff suppressed because it is too large Load diff

597
src/UIGridPyProperties.cpp Normal file
View 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
View 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."