Compare commits
3 commits
7d1066a5d5
...
a4e0b97ecb
| Author | SHA1 | Date | |
|---|---|---|---|
| a4e0b97ecb | |||
| 9a06ae5d8e | |||
| 2b0267430d |
10 changed files with 491 additions and 18 deletions
|
|
@ -5,7 +5,8 @@
|
|||
#include <algorithm>
|
||||
|
||||
PyTexture::PyTexture(std::string filename, int sprite_w, int sprite_h)
|
||||
: source(filename), sprite_width(sprite_w), sprite_height(sprite_h), sheet_width(0), sheet_height(0)
|
||||
: source(filename), sprite_width(sprite_w), sprite_height(sprite_h), sheet_width(0), sheet_height(0),
|
||||
display_width(-1), display_height(-1), display_offset_x(0), display_offset_y(0)
|
||||
{
|
||||
texture = sf::Texture();
|
||||
if (!texture.loadFromFile(source)) {
|
||||
|
|
@ -81,9 +82,14 @@ sf::Sprite PyTexture::sprite(int index, sf::Vector2f pos, sf::Vector2f s)
|
|||
// Return an empty sprite
|
||||
return sf::Sprite();
|
||||
}
|
||||
|
||||
|
||||
int tx = index % sheet_width, ty = index / sheet_width;
|
||||
auto ir = sf::IntRect(tx * sprite_width, ty * sprite_height, sprite_width, sprite_height);
|
||||
// #235: Apply display bounds within the cell
|
||||
int dw = getDisplayWidth();
|
||||
int dh = getDisplayHeight();
|
||||
auto ir = sf::IntRect(tx * sprite_width + display_offset_x,
|
||||
ty * sprite_height + display_offset_y,
|
||||
dw, dh);
|
||||
auto sprite = sf::Sprite(texture, ir);
|
||||
sprite.setPosition(pos);
|
||||
sprite.setScale(s);
|
||||
|
|
@ -138,21 +144,45 @@ Py_hash_t PyTexture::hash(PyObject* obj)
|
|||
|
||||
int PyTexture::init(PyTextureObject* self, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
static const char* keywords[] = { "filename", "sprite_width", "sprite_height", nullptr };
|
||||
static const char* keywords[] = { "filename", "sprite_width", "sprite_height",
|
||||
"display_size", "display_origin", nullptr };
|
||||
char* filename;
|
||||
int sprite_width, sprite_height;
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sii", const_cast<char**>(keywords), &filename, &sprite_width, &sprite_height))
|
||||
PyObject* display_size_obj = nullptr;
|
||||
PyObject* display_origin_obj = nullptr;
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sii|OO", const_cast<char**>(keywords),
|
||||
&filename, &sprite_width, &sprite_height, &display_size_obj, &display_origin_obj))
|
||||
return -1;
|
||||
|
||||
|
||||
// Create the texture object
|
||||
self->data = std::make_shared<PyTexture>(filename, sprite_width, sprite_height);
|
||||
|
||||
|
||||
// Check if the texture failed to load (sheet dimensions will be 0)
|
||||
if (self->data->sheet_width == 0 || self->data->sheet_height == 0) {
|
||||
PyErr_Format(PyExc_IOError, "Failed to load texture from file: %s", filename);
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
// #235: Parse optional display bounds
|
||||
if (display_size_obj && display_size_obj != Py_None) {
|
||||
int dw, dh;
|
||||
if (!PyArg_ParseTuple(display_size_obj, "ii", &dw, &dh)) {
|
||||
PyErr_SetString(PyExc_TypeError, "display_size must be a (width, height) tuple");
|
||||
return -1;
|
||||
}
|
||||
self->data->display_width = dw;
|
||||
self->data->display_height = dh;
|
||||
}
|
||||
if (display_origin_obj && display_origin_obj != Py_None) {
|
||||
int ox, oy;
|
||||
if (!PyArg_ParseTuple(display_origin_obj, "ii", &ox, &oy)) {
|
||||
PyErr_SetString(PyExc_TypeError, "display_origin must be an (x, y) tuple");
|
||||
return -1;
|
||||
}
|
||||
self->data->display_offset_x = ox;
|
||||
self->data->display_offset_y = oy;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -191,6 +221,26 @@ PyObject* PyTexture::get_source(PyTextureObject* self, void* closure)
|
|||
return PyUnicode_FromString(self->data->source.c_str());
|
||||
}
|
||||
|
||||
PyObject* PyTexture::get_display_width(PyTextureObject* self, void* closure)
|
||||
{
|
||||
return PyLong_FromLong(self->data->getDisplayWidth());
|
||||
}
|
||||
|
||||
PyObject* PyTexture::get_display_height(PyTextureObject* self, void* closure)
|
||||
{
|
||||
return PyLong_FromLong(self->data->getDisplayHeight());
|
||||
}
|
||||
|
||||
PyObject* PyTexture::get_display_offset_x(PyTextureObject* self, void* closure)
|
||||
{
|
||||
return PyLong_FromLong(self->data->display_offset_x);
|
||||
}
|
||||
|
||||
PyObject* PyTexture::get_display_offset_y(PyTextureObject* self, void* closure)
|
||||
{
|
||||
return PyLong_FromLong(self->data->display_offset_y);
|
||||
}
|
||||
|
||||
PyGetSetDef PyTexture::getsetters[] = {
|
||||
{"sprite_width", (getter)PyTexture::get_sprite_width, NULL,
|
||||
MCRF_PROPERTY(sprite_width, "Width of each sprite in pixels (int, read-only). Specified during texture initialization."), NULL},
|
||||
|
|
@ -204,6 +254,14 @@ PyGetSetDef PyTexture::getsetters[] = {
|
|||
MCRF_PROPERTY(sprite_count, "Total number of sprites in the texture sheet (int, read-only). Equals sheet_width * sheet_height."), NULL},
|
||||
{"source", (getter)PyTexture::get_source, NULL,
|
||||
MCRF_PROPERTY(source, "Source filename path (str, read-only). The path used to load this texture."), NULL},
|
||||
{"display_width", (getter)PyTexture::get_display_width, NULL,
|
||||
MCRF_PROPERTY(display_width, "Display width of sprite content within each cell (int, read-only). Defaults to sprite_width."), NULL},
|
||||
{"display_height", (getter)PyTexture::get_display_height, NULL,
|
||||
MCRF_PROPERTY(display_height, "Display height of sprite content within each cell (int, read-only). Defaults to sprite_height."), NULL},
|
||||
{"display_offset_x", (getter)PyTexture::get_display_offset_x, NULL,
|
||||
MCRF_PROPERTY(display_offset_x, "X offset of sprite content within each cell (int, read-only). Default 0."), NULL},
|
||||
{"display_offset_y", (getter)PyTexture::get_display_offset_y, NULL,
|
||||
MCRF_PROPERTY(display_offset_y, "Y offset of sprite content within each cell (int, read-only). Default 0."), NULL},
|
||||
{NULL} // Sentinel
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -17,10 +17,14 @@ private:
|
|||
int sheet_width, sheet_height;
|
||||
|
||||
// Private default constructor for factory methods
|
||||
PyTexture() : source("<uninitialized>"), sprite_width(0), sprite_height(0), sheet_width(0), sheet_height(0) {}
|
||||
PyTexture() : source("<uninitialized>"), sprite_width(0), sprite_height(0), sheet_width(0), sheet_height(0),
|
||||
display_width(-1), display_height(-1), display_offset_x(0), display_offset_y(0) {}
|
||||
|
||||
public:
|
||||
int sprite_width, sprite_height; // just use them read only, OK?
|
||||
// #235: Display bounds for non-uniform sprite content within cells
|
||||
int display_width, display_height; // -1 = same as sprite_width/height
|
||||
int display_offset_x, display_offset_y; // offset within cell to content area
|
||||
PyTexture(std::string filename, int sprite_w, int sprite_h);
|
||||
|
||||
// #144: Factory method to create texture from rendered content (snapshot)
|
||||
|
|
@ -42,6 +46,10 @@ public:
|
|||
static int init(PyTextureObject*, PyObject*, PyObject*);
|
||||
static PyObject* pynew(PyTypeObject* type, PyObject* args=NULL, PyObject* kwds=NULL);
|
||||
|
||||
// Effective display dimensions (resolves -1 defaults)
|
||||
int getDisplayWidth() const { return display_width >= 0 ? display_width : sprite_width; }
|
||||
int getDisplayHeight() const { return display_height >= 0 ? display_height : sprite_height; }
|
||||
|
||||
// Getters for properties
|
||||
static PyObject* get_sprite_width(PyTextureObject* self, void* closure);
|
||||
static PyObject* get_sprite_height(PyTextureObject* self, void* closure);
|
||||
|
|
@ -49,6 +57,10 @@ public:
|
|||
static PyObject* get_sheet_height(PyTextureObject* self, void* closure);
|
||||
static PyObject* get_sprite_count(PyTextureObject* self, void* closure);
|
||||
static PyObject* get_source(PyTextureObject* self, void* closure);
|
||||
static PyObject* get_display_width(PyTextureObject* self, void* closure);
|
||||
static PyObject* get_display_height(PyTextureObject* self, void* closure);
|
||||
static PyObject* get_display_offset_x(PyTextureObject* self, void* closure);
|
||||
static PyObject* get_display_offset_y(PyTextureObject* self, void* closure);
|
||||
|
||||
static PyGetSetDef getsetters[];
|
||||
|
||||
|
|
@ -69,17 +81,22 @@ namespace mcrfpydef {
|
|||
.tp_hash = PyTexture::hash,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = PyDoc_STR(
|
||||
"Texture(filename: str, sprite_width: int = 0, sprite_height: int = 0)\n\n"
|
||||
"Texture(filename: str, sprite_width: int = 0, sprite_height: int = 0, "
|
||||
"display_size: tuple = None, display_origin: tuple = None)\n\n"
|
||||
"A texture atlas for sprites and tiles.\n\n"
|
||||
"Args:\n"
|
||||
" filename: Path to an image file (PNG, BMP, etc.).\n"
|
||||
" sprite_width: Width of each sprite cell in pixels (0 = full image).\n"
|
||||
" sprite_height: Height of each sprite cell in pixels (0 = full image).\n\n"
|
||||
" sprite_height: Height of each sprite cell in pixels (0 = full image).\n"
|
||||
" display_size: Optional (w, h) actual content size within each cell.\n"
|
||||
" display_origin: Optional (x, y) content offset within each cell.\n\n"
|
||||
"Properties:\n"
|
||||
" sprite_width, sprite_height (int, read-only): Cell dimensions.\n"
|
||||
" sheet_width, sheet_height (int, read-only): Grid dimensions in cells.\n"
|
||||
" sprite_count (int, read-only): Total number of sprite cells.\n"
|
||||
" source (str, read-only): File path used to load this texture.\n"
|
||||
" display_width, display_height (int, read-only): Content size within cells.\n"
|
||||
" display_offset_x, display_offset_y (int, read-only): Content offset within cells.\n"
|
||||
),
|
||||
.tp_getset = PyTexture::getsetters,
|
||||
//.tp_base = &PyBaseObject_Type,
|
||||
|
|
|
|||
|
|
@ -115,9 +115,11 @@ std::vector<std::shared_ptr<UIEntity>> SpatialHash::queryCell(int x, int y) cons
|
|||
auto entity = wp.lock();
|
||||
if (!entity) continue;
|
||||
|
||||
// Match on cell_position (#295)
|
||||
if (entity->cell_position.x == x &&
|
||||
entity->cell_position.y == y) {
|
||||
// #236: Match on cell_position footprint for multi-tile entities
|
||||
if (x >= entity->cell_position.x &&
|
||||
x < entity->cell_position.x + entity->tile_width &&
|
||||
y >= entity->cell_position.y &&
|
||||
y < entity->cell_position.y + entity->tile_height) {
|
||||
result.push_back(entity);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -845,6 +845,60 @@ int UIEntity::set_sprite_offset_member(PyUIEntityObject* self, PyObject* value,
|
|||
return 0;
|
||||
}
|
||||
|
||||
// #236 - Multi-tile entity size
|
||||
PyObject* UIEntity::get_tile_size(PyUIEntityObject* self, void* closure) {
|
||||
return sfVector2f_to_PyObject(sf::Vector2f(
|
||||
static_cast<float>(self->data->tile_width),
|
||||
static_cast<float>(self->data->tile_height)));
|
||||
}
|
||||
|
||||
int UIEntity::set_tile_size(PyUIEntityObject* self, PyObject* value, void* closure) {
|
||||
sf::Vector2f vec = PyObject_to_sfVector2f(value);
|
||||
if (PyErr_Occurred()) return -1;
|
||||
int tw = static_cast<int>(vec.x);
|
||||
int th = static_cast<int>(vec.y);
|
||||
if (tw < 1 || th < 1) {
|
||||
PyErr_SetString(PyExc_ValueError, "tile_size components must be >= 1");
|
||||
return -1;
|
||||
}
|
||||
self->data->tile_width = tw;
|
||||
self->data->tile_height = th;
|
||||
if (self->data->grid) self->data->grid->markDirty();
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* UIEntity::get_tile_width(PyUIEntityObject* self, void* closure) {
|
||||
return PyLong_FromLong(self->data->tile_width);
|
||||
}
|
||||
|
||||
int UIEntity::set_tile_width(PyUIEntityObject* self, PyObject* value, void* closure) {
|
||||
int val = PyLong_AsLong(value);
|
||||
if (val == -1 && PyErr_Occurred()) return -1;
|
||||
if (val < 1) {
|
||||
PyErr_SetString(PyExc_ValueError, "tile_width must be >= 1");
|
||||
return -1;
|
||||
}
|
||||
self->data->tile_width = val;
|
||||
if (self->data->grid) self->data->grid->markDirty();
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* UIEntity::get_tile_height(PyUIEntityObject* self, void* closure) {
|
||||
return PyLong_FromLong(self->data->tile_height);
|
||||
}
|
||||
|
||||
int UIEntity::set_tile_height(PyUIEntityObject* self, PyObject* value, void* closure) {
|
||||
int val = PyLong_AsLong(value);
|
||||
if (val == -1 && PyErr_Occurred()) return -1;
|
||||
if (val < 1) {
|
||||
PyErr_SetString(PyExc_ValueError, "tile_height must be >= 1");
|
||||
return -1;
|
||||
}
|
||||
self->data->tile_height = val;
|
||||
if (self->data->grid) self->data->grid->markDirty();
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* UIEntity::die(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored))
|
||||
{
|
||||
// Check if entity has a grid
|
||||
|
|
@ -1596,6 +1650,13 @@ PyGetSetDef UIEntity::getsetters[] = {
|
|||
"X component of sprite pixel offset.", (void*)0},
|
||||
{"sprite_offset_y", (getter)UIEntity::get_sprite_offset_member, (setter)UIEntity::set_sprite_offset_member,
|
||||
"Y component of sprite pixel offset.", (void*)1},
|
||||
// #236 - Multi-tile entity size
|
||||
{"tile_size", (getter)UIEntity::get_tile_size, (setter)UIEntity::set_tile_size,
|
||||
"Entity size in tiles as (width, height) Vector. Default (1, 1).", NULL},
|
||||
{"tile_width", (getter)UIEntity::get_tile_width, (setter)UIEntity::set_tile_width,
|
||||
"Entity width in tiles (int). Must be >= 1. Default 1.", NULL},
|
||||
{"tile_height", (getter)UIEntity::get_tile_height, (setter)UIEntity::set_tile_height,
|
||||
"Entity height in tiles (int). Must be >= 1. Default 1.", NULL},
|
||||
// #296 - Label system
|
||||
{"labels", (getter)UIEntity::get_labels, (setter)UIEntity::set_labels,
|
||||
"Set of string labels for collision/targeting (frozenset). "
|
||||
|
|
|
|||
|
|
@ -71,6 +71,8 @@ public:
|
|||
sf::Vector2f position; //(x,y) in grid coordinates; float for animation
|
||||
sf::Vector2i cell_position{0, 0}; // #295: integer logical position (decoupled from float position)
|
||||
sf::Vector2f sprite_offset; // pixel offset for oversized sprites (applied pre-zoom)
|
||||
int tile_width = 1; // #236: entity size in tiles (for multi-tile entities)
|
||||
int tile_height = 1;
|
||||
std::unordered_set<std::string> labels; // #296: entity label system for collision/targeting
|
||||
PyObject* step_callback = nullptr; // #299: callback for grid.step() turn management
|
||||
int default_behavior = 0; // #299: BehaviorType::IDLE - behavior to revert to after DONE
|
||||
|
|
@ -169,6 +171,14 @@ public:
|
|||
static PyObject* get_sprite_offset_member(PyUIEntityObject* self, void* closure);
|
||||
static int set_sprite_offset_member(PyUIEntityObject* self, PyObject* value, void* closure);
|
||||
|
||||
// #236 - Multi-tile entity size
|
||||
static PyObject* get_tile_size(PyUIEntityObject* self, void* closure);
|
||||
static int set_tile_size(PyUIEntityObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_tile_width(PyUIEntityObject* self, void* closure);
|
||||
static int set_tile_width(PyUIEntityObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_tile_height(PyUIEntityObject* self, void* closure);
|
||||
static int set_tile_height(PyUIEntityObject* self, PyObject* value, void* closure);
|
||||
|
||||
// #295 - cell_pos (integer logical position)
|
||||
static PyObject* get_cell_pos(PyUIEntityObject* self, void* closure);
|
||||
static int set_cell_pos(PyUIEntityObject* self, PyObject* value, void* closure);
|
||||
|
|
|
|||
|
|
@ -198,10 +198,12 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
|
|||
int totalEntities = entities->size();
|
||||
|
||||
for (auto e : *entities) {
|
||||
// Skip out-of-bounds entities for performance
|
||||
// Check if entity is within visible bounds (with 2 cell margin for offset/oversized sprites)
|
||||
if (e->position.x < left_edge - 2 || e->position.x >= left_edge + width_sq + 2 ||
|
||||
e->position.y < top_edge - 2 || e->position.y >= top_edge + height_sq + 2) {
|
||||
// #236: Account for multi-tile entity size in frustum culling
|
||||
int margin = 2;
|
||||
if (e->position.x + e->tile_width < left_edge - margin ||
|
||||
e->position.x >= left_edge + width_sq + margin ||
|
||||
e->position.y + e->tile_height < top_edge - margin ||
|
||||
e->position.y >= top_edge + height_sq + margin) {
|
||||
continue; // Skip this entity as it's not visible
|
||||
}
|
||||
|
||||
|
|
|
|||
69
tests/regression/issue_268_vector_null_test.py
Normal file
69
tests/regression/issue_268_vector_null_test.py
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
"""Regression test: sfVector2f_to_PyObject must handle allocation failure.
|
||||
|
||||
Issue #268: sfVector2f_to_PyObject in UIEntity.cpp could return NULL when
|
||||
tp_alloc fails, but callers didn't check. Under normal conditions this
|
||||
can't be triggered, but this test exercises all Vector-returning entity
|
||||
properties to ensure the code path works correctly and doesn't crash.
|
||||
|
||||
Fix: Callers now propagate NULL returns as Python exceptions.
|
||||
"""
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
def test_entity_vector_properties():
|
||||
"""All Vector-returning properties must return valid Vector objects."""
|
||||
grid = mcrfpy.Grid(grid_size=(20, 20))
|
||||
entity = mcrfpy.Entity(grid_pos=(5, 5), grid=grid)
|
||||
|
||||
pos = entity.pos
|
||||
assert pos is not None, "entity.pos returned None"
|
||||
assert hasattr(pos, 'x') and hasattr(pos, 'y'), "pos missing x/y"
|
||||
|
||||
draw_pos = entity.draw_pos
|
||||
assert draw_pos is not None, "entity.draw_pos returned None"
|
||||
|
||||
cell_pos = entity.cell_pos
|
||||
assert cell_pos is not None, "entity.cell_pos returned None"
|
||||
assert cell_pos.x == 5 and cell_pos.y == 5, f"cell_pos wrong: {cell_pos.x}, {cell_pos.y}"
|
||||
|
||||
offset = entity.sprite_offset
|
||||
assert offset is not None, "entity.sprite_offset returned None"
|
||||
assert offset.x == 0.0 and offset.y == 0.0, "Default sprite_offset should be (0,0)"
|
||||
|
||||
print(" PASS: entity vector properties")
|
||||
|
||||
def test_entity_vector_setters():
|
||||
"""Vector property setters must accept tuples and Vectors."""
|
||||
grid = mcrfpy.Grid(grid_size=(20, 20))
|
||||
entity = mcrfpy.Entity(grid_pos=(3, 3), grid=grid)
|
||||
|
||||
entity.sprite_offset = (10.0, -5.0)
|
||||
assert entity.sprite_offset.x == 10.0, f"sprite_offset.x wrong: {entity.sprite_offset.x}"
|
||||
assert entity.sprite_offset.y == -5.0, f"sprite_offset.y wrong: {entity.sprite_offset.y}"
|
||||
|
||||
entity.draw_pos = mcrfpy.Vector(7.5, 8.5)
|
||||
assert entity.draw_pos.x == 7.5, f"draw_pos.x wrong: {entity.draw_pos.x}"
|
||||
|
||||
print(" PASS: entity vector setters")
|
||||
|
||||
def test_entity_no_grid_vector():
|
||||
"""Entities without a grid should handle vector properties gracefully."""
|
||||
entity = mcrfpy.Entity()
|
||||
|
||||
draw_pos = entity.draw_pos
|
||||
assert draw_pos is not None, "draw_pos should work without grid"
|
||||
|
||||
cell_pos = entity.cell_pos
|
||||
assert cell_pos is not None, "cell_pos should work without grid"
|
||||
|
||||
offset = entity.sprite_offset
|
||||
assert offset is not None, "sprite_offset should work without grid"
|
||||
|
||||
print(" PASS: entity no-grid vector properties")
|
||||
|
||||
print("Testing issue #268: sfVector2f_to_PyObject NULL safety...")
|
||||
test_entity_vector_properties()
|
||||
test_entity_vector_setters()
|
||||
test_entity_no_grid_vector()
|
||||
print("All #268 tests passed.")
|
||||
sys.exit(0)
|
||||
90
tests/regression/issue_272_uniform_owner_test.py
Normal file
90
tests/regression/issue_272_uniform_owner_test.py
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
"""Regression test: UniformCollection must check owner validity.
|
||||
|
||||
Issue #272: PyUniformCollectionObject stored a raw pointer to UniformCollection
|
||||
but only checked non-NULL. If the owning UIDrawable was destroyed, the raw
|
||||
pointer dangled, causing use-after-free.
|
||||
|
||||
Fix: Added weak_ptr<void> owner field. All accessors now check owner.lock()
|
||||
before accessing the collection, raising RuntimeError if the owner is gone.
|
||||
"""
|
||||
import mcrfpy
|
||||
import gc
|
||||
import sys
|
||||
|
||||
def test_uniforms_basic_access():
|
||||
"""Uniform collection should work when owner is alive."""
|
||||
frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100))
|
||||
u = frame.uniforms
|
||||
assert len(u) == 0, f"Expected empty uniforms, got {len(u)}"
|
||||
|
||||
u['brightness'] = 1.0
|
||||
assert len(u) == 1, f"Expected 1 uniform, got {len(u)}"
|
||||
assert 'brightness' in u, "brightness should be in uniforms"
|
||||
|
||||
del u['brightness']
|
||||
assert len(u) == 0, f"Expected 0 after delete, got {len(u)}"
|
||||
|
||||
print(" PASS: basic uniform access")
|
||||
|
||||
def test_uniforms_owner_destroyed():
|
||||
"""Accessing uniforms after owner destruction must fail gracefully."""
|
||||
frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100))
|
||||
u = frame.uniforms
|
||||
u['test'] = 1.0
|
||||
|
||||
del frame
|
||||
gc.collect()
|
||||
|
||||
# len() and __contains__ return 0/False gracefully
|
||||
assert len(u) == 0, f"len should be 0 after owner destroyed, got {len(u)}"
|
||||
assert 'test' not in u, "'test' should not be found after owner destroyed"
|
||||
|
||||
# Subscript access and assignment raise RuntimeError
|
||||
errors = 0
|
||||
try:
|
||||
_ = u['test']
|
||||
except RuntimeError:
|
||||
errors += 1
|
||||
|
||||
try:
|
||||
u['new'] = 2.0
|
||||
except RuntimeError:
|
||||
errors += 1
|
||||
|
||||
assert errors == 2, f"Expected 2 RuntimeErrors from subscript ops, got {errors}"
|
||||
print(" PASS: uniforms after owner destroyed")
|
||||
|
||||
def test_uniforms_owner_in_scene():
|
||||
"""Uniforms should work while owner is in a scene's children."""
|
||||
scene = mcrfpy.Scene("uniform_test")
|
||||
frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100))
|
||||
scene.children.append(frame)
|
||||
u = frame.uniforms
|
||||
u['value'] = 0.5
|
||||
assert len(u) == 1, "Uniform should be accessible while in scene"
|
||||
print(" PASS: uniforms with scene-owned frame")
|
||||
|
||||
def test_uniforms_multiple_values():
|
||||
"""Multiple uniform types should work correctly."""
|
||||
frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100))
|
||||
u = frame.uniforms
|
||||
|
||||
u['f'] = 1.0
|
||||
u['v2'] = (1.0, 2.0)
|
||||
u['v3'] = (1.0, 2.0, 3.0)
|
||||
u['v4'] = (1.0, 2.0, 3.0, 4.0)
|
||||
|
||||
assert len(u) == 4, f"Expected 4 uniforms, got {len(u)}"
|
||||
|
||||
keys = list(u.keys())
|
||||
assert len(keys) == 4, f"Expected 4 keys, got {len(keys)}"
|
||||
|
||||
print(" PASS: multiple uniform types")
|
||||
|
||||
print("Testing issue #272: UniformCollection owner validity...")
|
||||
test_uniforms_basic_access()
|
||||
test_uniforms_owner_destroyed()
|
||||
test_uniforms_owner_in_scene()
|
||||
test_uniforms_multiple_values()
|
||||
print("All #272 tests passed.")
|
||||
sys.exit(0)
|
||||
104
tests/unit/multi_tile_entity_test.py
Normal file
104
tests/unit/multi_tile_entity_test.py
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
"""Test multi-tile entity rendering and positioning (#236).
|
||||
|
||||
Verifies that entities can span multiple grid cells using tile_width
|
||||
and tile_height properties, with correct frustum culling and spatial
|
||||
hash queries.
|
||||
"""
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
def test_default_tile_size():
|
||||
"""New entities default to 1x1 tile size."""
|
||||
entity = mcrfpy.Entity()
|
||||
assert entity.tile_width == 1, f"Expected 1, got {entity.tile_width}"
|
||||
assert entity.tile_height == 1, f"Expected 1, got {entity.tile_height}"
|
||||
ts = entity.tile_size
|
||||
assert ts.x == 1.0 and ts.y == 1.0, f"tile_size wrong: ({ts.x}, {ts.y})"
|
||||
print(" PASS: default tile size")
|
||||
|
||||
def test_set_tile_size_individual():
|
||||
"""tile_width and tile_height can be set individually."""
|
||||
entity = mcrfpy.Entity()
|
||||
entity.tile_width = 2
|
||||
entity.tile_height = 3
|
||||
assert entity.tile_width == 2, f"Expected 2, got {entity.tile_width}"
|
||||
assert entity.tile_height == 3, f"Expected 3, got {entity.tile_height}"
|
||||
print(" PASS: individual tile size setters")
|
||||
|
||||
def test_set_tile_size_tuple():
|
||||
"""tile_size can be set as a tuple."""
|
||||
entity = mcrfpy.Entity()
|
||||
entity.tile_size = (4, 2)
|
||||
assert entity.tile_width == 4, f"Expected 4, got {entity.tile_width}"
|
||||
assert entity.tile_height == 2, f"Expected 2, got {entity.tile_height}"
|
||||
print(" PASS: tile_size tuple setter")
|
||||
|
||||
def test_tile_size_validation():
|
||||
"""tile_width/height must be >= 1."""
|
||||
entity = mcrfpy.Entity()
|
||||
try:
|
||||
entity.tile_width = 0
|
||||
assert False, "Should have raised ValueError"
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
entity.tile_height = -1
|
||||
assert False, "Should have raised ValueError"
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
entity.tile_size = (0, 1)
|
||||
assert False, "Should have raised ValueError"
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
print(" PASS: tile size validation")
|
||||
|
||||
def test_multi_tile_in_grid():
|
||||
"""Multi-tile entities work correctly in grids."""
|
||||
grid = mcrfpy.Grid(grid_size=(20, 20))
|
||||
entity = mcrfpy.Entity(grid_pos=(5, 5), grid=grid)
|
||||
entity.tile_width = 2
|
||||
entity.tile_height = 2
|
||||
|
||||
assert entity.tile_width == 2
|
||||
assert entity.tile_height == 2
|
||||
assert entity.cell_pos.x == 5
|
||||
assert entity.cell_pos.y == 5
|
||||
print(" PASS: multi-tile entity in grid")
|
||||
|
||||
def test_spatial_hash_multi_tile():
|
||||
"""Spatial hash queries find multi-tile entities at covered cells."""
|
||||
grid = mcrfpy.Grid(grid_size=(20, 20))
|
||||
entity = mcrfpy.Entity(grid_pos=(5, 5), grid=grid)
|
||||
entity.tile_width = 2
|
||||
entity.tile_height = 2
|
||||
|
||||
at_origin = grid.at(5, 5).entities
|
||||
assert len(at_origin) >= 1, f"Entity not found at origin (5,5): {len(at_origin)}"
|
||||
|
||||
at_right = grid.at(6, 5).entities
|
||||
assert len(at_right) >= 1, f"Entity not found at covered cell (6,5): {len(at_right)}"
|
||||
|
||||
at_below = grid.at(5, 6).entities
|
||||
assert len(at_below) >= 1, f"Entity not found at covered cell (5,6): {len(at_below)}"
|
||||
|
||||
at_corner = grid.at(6, 6).entities
|
||||
assert len(at_corner) >= 1, f"Entity not found at covered cell (6,6): {len(at_corner)}"
|
||||
|
||||
at_outside = grid.at(7, 5).entities
|
||||
assert len(at_outside) == 0, f"Entity found outside footprint (7,5): {len(at_outside)}"
|
||||
|
||||
print(" PASS: spatial hash multi-tile queries")
|
||||
|
||||
print("Testing #236: Multi-tile entities...")
|
||||
test_default_tile_size()
|
||||
test_set_tile_size_individual()
|
||||
test_set_tile_size_tuple()
|
||||
test_tile_size_validation()
|
||||
test_multi_tile_in_grid()
|
||||
test_spatial_hash_multi_tile()
|
||||
print("All #236 tests passed.")
|
||||
sys.exit(0)
|
||||
60
tests/unit/texture_display_bounds_test.py
Normal file
60
tests/unit/texture_display_bounds_test.py
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
"""Test texture display bounds for non-uniform sprite content (#235).
|
||||
|
||||
Verifies that Texture accepts display_size and display_origin parameters
|
||||
to crop sprite rendering to a sub-region within each atlas cell.
|
||||
"""
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
def test_default_display_bounds():
|
||||
"""Without display bounds, display dims equal sprite dims."""
|
||||
tex = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||
assert tex.display_width == 16, f"Expected 16, got {tex.display_width}"
|
||||
assert tex.display_height == 16, f"Expected 16, got {tex.display_height}"
|
||||
assert tex.display_offset_x == 0, f"Expected 0, got {tex.display_offset_x}"
|
||||
assert tex.display_offset_y == 0, f"Expected 0, got {tex.display_offset_y}"
|
||||
print(" PASS: default display bounds")
|
||||
|
||||
def test_custom_display_size():
|
||||
"""display_size crops sprite content within cells."""
|
||||
tex = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16,
|
||||
display_size=(12, 14))
|
||||
assert tex.display_width == 12, f"Expected 12, got {tex.display_width}"
|
||||
assert tex.display_height == 14, f"Expected 14, got {tex.display_height}"
|
||||
assert tex.sprite_width == 16, f"sprite_width should be unchanged: {tex.sprite_width}"
|
||||
assert tex.sprite_height == 16, f"sprite_height should be unchanged: {tex.sprite_height}"
|
||||
print(" PASS: custom display size")
|
||||
|
||||
def test_custom_display_origin():
|
||||
"""display_origin offsets content within cells."""
|
||||
tex = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16,
|
||||
display_size=(12, 14), display_origin=(2, 1))
|
||||
assert tex.display_offset_x == 2, f"Expected 2, got {tex.display_offset_x}"
|
||||
assert tex.display_offset_y == 1, f"Expected 1, got {tex.display_offset_y}"
|
||||
print(" PASS: custom display origin")
|
||||
|
||||
def test_display_bounds_sprite_creation():
|
||||
"""Sprites created from bounded textures should work in UI elements."""
|
||||
tex = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16,
|
||||
display_size=(12, 14), display_origin=(2, 1))
|
||||
sprite = mcrfpy.Sprite(pos=(10, 10), texture=tex, sprite_index=0)
|
||||
assert sprite is not None, "Sprite creation with display bounds failed"
|
||||
print(" PASS: sprite creation with display bounds")
|
||||
|
||||
def test_display_bounds_in_grid():
|
||||
"""Entities using bounded textures should render in grids."""
|
||||
tex = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16,
|
||||
display_size=(12, 14), display_origin=(2, 1))
|
||||
grid = mcrfpy.Grid(grid_size=(10, 10), texture=tex)
|
||||
entity = mcrfpy.Entity(grid_pos=(3, 3), texture=tex, sprite_index=5, grid=grid)
|
||||
assert entity is not None, "Entity creation with display bounds failed"
|
||||
print(" PASS: entity with display bounds in grid")
|
||||
|
||||
print("Testing #235: Texture display bounds...")
|
||||
test_default_display_bounds()
|
||||
test_custom_display_size()
|
||||
test_custom_display_origin()
|
||||
test_display_bounds_sprite_creation()
|
||||
test_display_bounds_in_grid()
|
||||
print("All #235 tests passed.")
|
||||
sys.exit(0)
|
||||
Loading…
Add table
Add a link
Reference in a new issue