From 1c71d8d4f743900bf2bef097b3d1addf64dbe04a Mon Sep 17 00:00:00 2001 From: John McCardle Date: Thu, 3 Jul 2025 19:36:15 -0400 Subject: [PATCH] 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 --- src/UIGrid.cpp | 77 ++++++++++++++++-------- src/UIGrid.h | 3 + tests/ui_Grid_none_texture_test.py | 97 ++++++++++++++++++++++++++++++ tests/ui_Grid_null_texture_test.py | 35 +++++++++++ 4 files changed, 186 insertions(+), 26 deletions(-) create mode 100644 tests/ui_Grid_none_texture_test.py create mode 100644 tests/ui_Grid_null_texture_test.py diff --git a/src/UIGrid.cpp b/src/UIGrid.cpp index 94dd481..64e928b 100644 --- a/src/UIGrid.cpp +++ b/src/UIGrid.cpp @@ -6,9 +6,15 @@ UIGrid::UIGrid() {} UIGrid::UIGrid(int gx, int gy, std::shared_ptr _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>>(); box.setSize(_wh); @@ -18,7 +24,10 @@ UIGrid::UIGrid(int gx, int gy, std::shared_ptr _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(textureObj); - // TODO (7DRL day 2, item 4.) use shared_ptr / PyTextureObject on UIGrid - //IndexTexture* texture = pyTexture->data.get(); - // Initialize UIGrid + std::shared_ptr 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(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(grid_x, grid_y, pyTexture->data, // sf::Vector2f(box_x, box_y), sf::Vector2f(box_w, box_h)); - self->data = std::make_shared(grid_x, grid_y, pyTexture->data, pos_result->data, size_result->data); + self->data = std::make_shared(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) { diff --git a/src/UIGrid.h b/src/UIGrid.h index 410fea3..300c2f5 100644 --- a/src/UIGrid.h +++ b/src/UIGrid.h @@ -21,6 +21,9 @@ class UIGrid: public UIDrawable { private: std::shared_ptr 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); diff --git a/tests/ui_Grid_none_texture_test.py b/tests/ui_Grid_none_texture_test.py new file mode 100644 index 0000000..38150ef --- /dev/null +++ b/tests/ui_Grid_none_texture_test.py @@ -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...") \ No newline at end of file diff --git a/tests/ui_Grid_null_texture_test.py b/tests/ui_Grid_null_texture_test.py new file mode 100644 index 0000000..fdac956 --- /dev/null +++ b/tests/ui_Grid_null_texture_test.py @@ -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() \ No newline at end of file