Fix Grid to support None/null texture and fix error message bug

- Allow Grid to be created with None as texture parameter
- Use default cell dimensions (16x16) when no texture provided
- Skip sprite rendering when texture is null, but still render colors
- Fix issue #77: Corrected copy/paste error in Grid.at() error messages
- Grid now functional for color-only rendering and entity positioning

Test created to verify Grid works without texture, showing colored cells.

Closes #77
This commit is contained in:
John McCardle 2025-07-03 19:36:15 -04:00
parent 18cfe93a44
commit 1c71d8d4f7
4 changed files with 186 additions and 26 deletions

View File

@ -6,9 +6,15 @@ UIGrid::UIGrid() {}
UIGrid::UIGrid(int gx, int gy, std::shared_ptr<PyTexture> _ptex, sf::Vector2f _xy, sf::Vector2f _wh)
: grid_x(gx), grid_y(gy),
zoom(1.0f), center_x((gx/2) * _ptex->sprite_width), center_y((gy/2) * _ptex->sprite_height),
zoom(1.0f),
ptex(_ptex), points(gx * gy)
{
// Use texture dimensions if available, otherwise use defaults
int cell_width = _ptex ? _ptex->sprite_width : DEFAULT_CELL_WIDTH;
int cell_height = _ptex ? _ptex->sprite_height : DEFAULT_CELL_HEIGHT;
center_x = (gx/2) * cell_width;
center_y = (gy/2) * cell_height;
entities = std::make_shared<std::list<std::shared_ptr<UIEntity>>>();
box.setSize(_wh);
@ -18,7 +24,10 @@ UIGrid::UIGrid(int gx, int gy, std::shared_ptr<PyTexture> _ptex, sf::Vector2f _x
// create renderTexture with maximum theoretical size; sprite can resize to show whatever amount needs to be rendered
renderTexture.create(1920, 1080); // TODO - renderTexture should be window size; above 1080p this will cause rendering errors
sprite = ptex->sprite(0);
// Only initialize sprite if texture is available
if (ptex) {
sprite = ptex->sprite(0);
}
output.setTextureRect(
sf::IntRect(0, 0,
@ -40,12 +49,17 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
sf::IntRect(0, 0,
box.getSize().x, box.getSize().y));
renderTexture.clear(sf::Color(8, 8, 8, 255)); // TODO - UIGrid needs a "background color" field
// Get cell dimensions - use texture if available, otherwise defaults
int cell_width = ptex ? ptex->sprite_width : DEFAULT_CELL_WIDTH;
int cell_height = ptex ? ptex->sprite_height : DEFAULT_CELL_HEIGHT;
// sprites that are visible according to zoom, center_x, center_y, and box width
float center_x_sq = center_x / ptex->sprite_width;
float center_y_sq = center_y / ptex->sprite_height;
float center_x_sq = center_x / cell_width;
float center_y_sq = center_y / cell_height;
float width_sq = box.getSize().x / (ptex->sprite_width * zoom);
float height_sq = box.getSize().y / (ptex->sprite_height * zoom);
float width_sq = box.getSize().x / (cell_width * zoom);
float height_sq = box.getSize().y / (cell_height * zoom);
float left_edge = center_x_sq - (width_sq / 2.0);
float top_edge = center_y_sq - (height_sq / 2.0);
@ -54,7 +68,7 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
//sprite.setScale(sf::Vector2f(zoom, zoom));
sf::RectangleShape r; // for colors and overlays
r.setSize(sf::Vector2f(ptex->sprite_width * zoom, ptex->sprite_height * zoom));
r.setSize(sf::Vector2f(cell_width * zoom, cell_height * zoom));
r.setOutlineThickness(0);
int x_limit = left_edge + width_sq + 2;
@ -74,8 +88,8 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
y+=1)
{
auto pixel_pos = sf::Vector2f(
(x*ptex->sprite_width - left_spritepixels) * zoom,
(y*ptex->sprite_height - top_spritepixels) * zoom );
(x*cell_width - left_spritepixels) * zoom,
(y*cell_height - top_spritepixels) * zoom );
auto gridpoint = at(std::floor(x), std::floor(y));
@ -85,10 +99,10 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
r.setFillColor(gridpoint.color);
renderTexture.draw(r);
// tilesprite
// tilesprite - only draw if texture is available
// if discovered but not visible, set opacity to 90%
// if not discovered... just don't draw it?
if (gridpoint.tilesprite != -1) {
if (ptex && gridpoint.tilesprite != -1) {
sprite = ptex->sprite(gridpoint.tilesprite, pixel_pos, sf::Vector2f(zoom, zoom)); //setSprite(gridpoint.tilesprite);;
renderTexture.draw(sprite);
}
@ -104,8 +118,8 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
//drawent.setScale(zoom, zoom);
drawent.setScale(sf::Vector2f(zoom, zoom));
auto pixel_pos = sf::Vector2f(
(e->position.x*ptex->sprite_width - left_spritepixels) * zoom,
(e->position.y*ptex->sprite_height - top_spritepixels) * zoom );
(e->position.x*cell_width - left_spritepixels) * zoom,
(e->position.y*cell_height - top_spritepixels) * zoom );
//drawent.setPosition(pixel_pos);
//renderTexture.draw(drawent);
drawent.render(pixel_pos, renderTexture);
@ -229,21 +243,25 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
// Convert PyObject texture to IndexTexture*
// This requires the texture object to have been initialized similar to UISprite's texture handling
//if (!PyObject_IsInstance(textureObj, (PyObject*)&PyTextureType)) {
if (!PyObject_IsInstance(textureObj, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))) {
PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance");
return -1;
}
PyTextureObject* pyTexture = reinterpret_cast<PyTextureObject*>(textureObj);
// TODO (7DRL day 2, item 4.) use shared_ptr / PyTextureObject on UIGrid
//IndexTexture* texture = pyTexture->data.get();
// Initialize UIGrid
std::shared_ptr<PyTexture> texture_ptr = nullptr;
// Allow None for texture
if (textureObj != Py_None) {
//if (!PyObject_IsInstance(textureObj, (PyObject*)&PyTextureType)) {
if (!PyObject_IsInstance(textureObj, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))) {
PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None");
return -1;
}
PyTextureObject* pyTexture = reinterpret_cast<PyTextureObject*>(textureObj);
texture_ptr = pyTexture->data;
}
// Initialize UIGrid - texture_ptr will be nullptr if texture was None
//self->data = new UIGrid(grid_x, grid_y, texture, sf::Vector2f(box_x, box_y), sf::Vector2f(box_w, box_h));
//self->data = std::make_shared<UIGrid>(grid_x, grid_y, pyTexture->data,
// sf::Vector2f(box_x, box_y), sf::Vector2f(box_w, box_h));
self->data = std::make_shared<UIGrid>(grid_x, grid_y, pyTexture->data, pos_result->data, size_result->data);
self->data = std::make_shared<UIGrid>(grid_x, grid_y, texture_ptr, pos_result->data, size_result->data);
return 0; // Success
}
@ -365,9 +383,16 @@ PyObject* UIGrid::get_texture(PyUIGridObject* self, void* closure) {
//return self->data->getTexture()->pyObject();
// PyObject_GetAttrString(McRFPy_API::mcrf_module, "GridPointState")
//PyTextureObject* obj = (PyTextureObject*)((&PyTextureType)->tp_alloc(&PyTextureType, 0));
// Return None if no texture
auto texture = self->data->getTexture();
if (!texture) {
Py_RETURN_NONE;
}
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture");
auto obj = (PyTextureObject*)type->tp_alloc(type, 0);
obj->data = self->data->getTexture();
obj->data = texture;
return (PyObject*)obj;
}
@ -379,7 +404,7 @@ PyObject* UIGrid::py_at(PyUIGridObject* self, PyObject* o)
return NULL;
}
if (x < 0 || x >= self->data->grid_x) {
PyErr_SetString(PyExc_ValueError, "x value out of range (0, Grid.grid_y)");
PyErr_SetString(PyExc_ValueError, "x value out of range (0, Grid.grid_x)");
return NULL;
}
if (y < 0 || y >= self->data->grid_y) {

View File

@ -21,6 +21,9 @@ class UIGrid: public UIDrawable
{
private:
std::shared_ptr<PyTexture> ptex;
// Default cell dimensions when no texture is provided
static constexpr int DEFAULT_CELL_WIDTH = 16;
static constexpr int DEFAULT_CELL_HEIGHT = 16;
public:
UIGrid();
//UIGrid(int, int, IndexTexture*, float, float, float, float);

View File

@ -0,0 +1,97 @@
#!/usr/bin/env python3
"""Test Grid creation with None texture - should work with color cells only"""
import mcrfpy
from mcrfpy import automation
import sys
def test_grid_none_texture(runtime):
"""Test Grid functionality without texture"""
print("\n=== Testing Grid with None texture ===")
# Test 1: Create Grid with None texture
try:
grid = mcrfpy.Grid(10, 10, None, mcrfpy.Vector(50, 50), mcrfpy.Vector(400, 400))
print("✓ Grid created successfully with None texture")
except Exception as e:
print(f"✗ Failed to create Grid with None texture: {e}")
sys.exit(1)
# Add to UI
ui = mcrfpy.sceneUI("grid_none_test")
ui.append(grid)
# Test 2: Verify grid properties
try:
grid_size = grid.grid_size
print(f"✓ Grid size: {grid_size}")
# Check texture property
texture = grid.texture
if texture is None:
print("✓ Grid texture is None as expected")
else:
print(f"✗ Grid texture should be None, got: {texture}")
except Exception as e:
print(f"✗ Property access failed: {e}")
# Test 3: Access grid points and set colors
try:
# Create a checkerboard pattern with colors
for x in range(10):
for y in range(10):
point = grid.at(x, y)
if (x + y) % 2 == 0:
point.color = mcrfpy.Color(255, 0, 0, 255) # Red
else:
point.color = mcrfpy.Color(0, 0, 255, 255) # Blue
print("✓ Successfully set grid point colors")
except Exception as e:
print(f"✗ Failed to set grid colors: {e}")
# Test 4: Add entities to the grid
try:
# Create an entity with its own texture
entity_texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
entity = mcrfpy.Entity(mcrfpy.Vector(5, 5), entity_texture, 1, grid)
grid.entities.append(entity)
print(f"✓ Added entity to grid, total entities: {len(grid.entities)}")
except Exception as e:
print(f"✗ Failed to add entity: {e}")
# Test 5: Test grid interaction properties
try:
# Test zoom
grid.zoom = 2.0
print(f"✓ Set zoom to: {grid.zoom}")
# Test center
grid.center = mcrfpy.Vector(5, 5)
print(f"✓ Set center to: {grid.center}")
except Exception as e:
print(f"✗ Grid properties failed: {e}")
# Take screenshot
filename = f"grid_none_texture_test_{int(runtime)}.png"
result = automation.screenshot(filename)
print(f"\nScreenshot saved: {filename} - Result: {result}")
print("The grid should show a red/blue checkerboard pattern")
print("\n✓ PASS - Grid works correctly without texture!")
sys.exit(0)
# Set up test scene
print("Creating test scene...")
mcrfpy.createScene("grid_none_test")
mcrfpy.setScene("grid_none_test")
# Add a background frame so we can see the grid
ui = mcrfpy.sceneUI("grid_none_test")
background = mcrfpy.Frame(0, 0, 800, 600,
fill_color=mcrfpy.Color(200, 200, 200),
outline_color=mcrfpy.Color(0, 0, 0),
outline=2.0)
ui.append(background)
# Schedule test
mcrfpy.setTimer("test", test_grid_none_texture, 100)
print("Test scheduled...")

View File

@ -0,0 +1,35 @@
#!/usr/bin/env python3
"""Test Grid creation with null/None texture to reproduce segfault"""
import mcrfpy
import sys
def test_grid_null_texture():
"""Test if Grid can be created without a texture"""
print("=== Testing Grid with null texture ===")
# Create test scene
mcrfpy.createScene("grid_null_test")
mcrfpy.setScene("grid_null_test")
ui = mcrfpy.sceneUI("grid_null_test")
# Test 1: Try with None
try:
print("Test 1: Creating Grid with None texture...")
grid = mcrfpy.Grid(10, 10, None, mcrfpy.Vector(0, 0), mcrfpy.Vector(400, 400))
print("✗ Should have raised exception for None texture")
except Exception as e:
print(f"✓ Correctly rejected None texture: {e}")
# Test 2: Try without texture parameter (if possible)
try:
print("\nTest 2: Creating Grid with missing parameters...")
grid = mcrfpy.Grid(10, 10)
print("✗ Should have raised exception for missing parameters")
except Exception as e:
print(f"✓ Correctly rejected missing parameters: {e}")
print("\nTest complete - Grid requires texture parameter")
sys.exit(0)
# Run immediately
test_grid_null_texture()