Fix verify-pass code bugs #317/#318/#319

Three small bugs surfaced by the #314 docstring-accuracy verify pass:

#317 automation.scroll() dropped the x of its position argument: scroll()
resolved (x, y) but called injectMouseEvent(MouseWheelScrolled, clicks, y),
passing the scroll amount as x. injectMouseEvent now takes the scroll delta as
its own parameter and scroll() forwards the real x/y.

#318 GridView.texture always returned None (a TODO stub). It now returns a
Texture wrapper sharing the underlying shared_ptr<PyTexture>, mirroring
Grid.texture. (mcrfpy.Grid and mcrfpy.GridView are the same type post-#252, so
this fixes both names.)

#319 Entity.visible_entities(radius=None) raised TypeError: radius was parsed
with the 'i' format code, which rejects None. It now parses radius as an object
and treats None / omitted / -1 as "use the grid's default fov_radius"; a
non-int, non-None radius raises a clear TypeError.

- regression tests for each under tests/regression/
- api_surface snapshot re-baselined (visible_entities signature; texture
  property now Texture | None) and docs/stubs regenerated; frozen docstring
  gate still 100%

closes #317
closes #318
closes #319

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KnywUddaFRhkxo5kijxJnv
This commit is contained in:
John McCardle 2026-06-21 10:12:41 -04:00
commit 98489a96fd
12 changed files with 303 additions and 55 deletions

View file

@ -1,6 +1,6 @@
# McRogueFace API Reference # McRogueFace API Reference
*Generated on 2026-06-21 09:39:04* *Generated on 2026-06-21 10:11:50*
*This documentation was dynamically generated from the compiled module.* *This documentation was dynamically generated from the compiled module.*
@ -1989,19 +1989,17 @@ Note:
**Returns:** None Called automatically when the entity moves if the grid has FOV configured. **Returns:** None Called automatically when the entity moves if the grid has FOV configured.
#### `visible_entities(fov=None, radius: int = -1) -> list[Entity]` #### `visible_entities(fov=None, radius: int | None = None) -> list[Entity]`
Get list of other entities visible from this entity's position. Get list of other entities visible from this entity's position.
Note:
**Arguments:** **Arguments:**
- `fov`: FOV algorithm to use (FOV enum or None to use grid.fov) - `fov`: FOV algorithm to use (FOV enum or None to use grid.fov)
- `radius`: FOV radius as int; omit or pass -1 to use the grid's default fov_radius - `radius`: FOV radius as int; pass None, omit, or pass -1 to use the grid's default fov_radius
**Returns:** List of Entity objects within field of view, excluding self **Returns:** List of Entity objects within field of view, excluding self
**Raises:** ValueError: If entity is not associated with a grid radius does not accept None; omit the argument entirely to use the grid default. **Raises:** ValueError: If entity is not associated with a grid TypeError: If radius is neither an int nor None
### Entity3D ### Entity3D
@ -2441,7 +2439,7 @@ Keyword Args:
- `rotate_with_camera`: Whether to rotate visually with parent Grid's camera_rotation (bool). False (default): stay screen-aligned. True: tilt with camera. Only affects children of UIGrid; ignored for other parents. - `rotate_with_camera`: Whether to rotate visually with parent Grid's camera_rotation (bool). False (default): stay screen-aligned. True: tilt with camera. Only affects children of UIGrid; ignored for other parents.
- `rotation`: Rotation angle in degrees (clockwise around origin). Animatable property. - `rotation`: Rotation angle in degrees (clockwise around origin). Animatable property.
- `shader`: Shader for GPU visual effects (Shader or None). When set, the drawable is rendered through the shader program. Set to None to disable shader effects. - `shader`: Shader for GPU visual effects (Shader or None). When set, the drawable is rendered through the shader program. Set to None to disable shader effects.
- `texture` *(read-only)*: Texture used for tile rendering (None, read-only). Texture return is not yet implemented; always returns None. - `texture` *(read-only)*: Texture used for tile rendering (Texture | None, read-only).
- `uniforms` *(read-only)*: Collection of shader uniforms (read-only access to collection). Set uniforms via dict-like syntax: drawable.uniforms['name'] = value. Supports float, vec2/3/4 tuples, PropertyBinding, and CallableBinding. - `uniforms` *(read-only)*: Collection of shader uniforms (read-only access to collection). Set uniforms via dict-like syntax: drawable.uniforms['name'] = value. Supports float, vec2/3/4 tuples, PropertyBinding, and CallableBinding.
- `vert_margin`: Vertical margin override (float, 0 = use general margin). Invalid for horizontally-centered alignments (CENTER_LEFT, CENTER_RIGHT, CENTER). - `vert_margin`: Vertical margin override (float, 0 = use general margin). Invalid for horizontally-centered alignments (CENTER_LEFT, CENTER_RIGHT, CENTER).
- `visible`: Whether the object is visible (bool). Invisible objects are not rendered or clickable. - `visible`: Whether the object is visible (bool). Invisible objects are not rendered or clickable.
@ -2561,7 +2559,7 @@ Keyword Args:
- `rotate_with_camera`: Whether to rotate visually with parent Grid's camera_rotation (bool). False (default): stay screen-aligned. True: tilt with camera. Only affects children of UIGrid; ignored for other parents. - `rotate_with_camera`: Whether to rotate visually with parent Grid's camera_rotation (bool). False (default): stay screen-aligned. True: tilt with camera. Only affects children of UIGrid; ignored for other parents.
- `rotation`: Rotation angle in degrees (clockwise around origin). Animatable property. - `rotation`: Rotation angle in degrees (clockwise around origin). Animatable property.
- `shader`: Shader for GPU visual effects (Shader or None). When set, the drawable is rendered through the shader program. Set to None to disable shader effects. - `shader`: Shader for GPU visual effects (Shader or None). When set, the drawable is rendered through the shader program. Set to None to disable shader effects.
- `texture` *(read-only)*: Texture used for tile rendering (None, read-only). Texture return is not yet implemented; always returns None. - `texture` *(read-only)*: Texture used for tile rendering (Texture | None, read-only).
- `uniforms` *(read-only)*: Collection of shader uniforms (read-only access to collection). Set uniforms via dict-like syntax: drawable.uniforms['name'] = value. Supports float, vec2/3/4 tuples, PropertyBinding, and CallableBinding. - `uniforms` *(read-only)*: Collection of shader uniforms (read-only access to collection). Set uniforms via dict-like syntax: drawable.uniforms['name'] = value. Supports float, vec2/3/4 tuples, PropertyBinding, and CallableBinding.
- `vert_margin`: Vertical margin override (float, 0 = use general margin). Invalid for horizontally-centered alignments (CENTER_LEFT, CENTER_RIGHT, CENTER). - `vert_margin`: Vertical margin override (float, 0 = use general margin). Invalid for horizontally-centered alignments (CENTER_LEFT, CENTER_RIGHT, CENTER).
- `visible`: Whether the object is visible (bool). Invisible objects are not rendered or clickable. - `visible`: Whether the object is visible (bool). Invisible objects are not rendered or clickable.

View file

@ -108,7 +108,7 @@
<body> <body>
<div class="container"> <div class="container">
<h1>McRogueFace API Reference</h1> <h1>McRogueFace API Reference</h1>
<p><em>Generated on 2026-06-21 09:39:04</em></p> <p><em>Generated on 2026-06-21 10:11:50</em></p>
<p><em>This documentation was dynamically generated from the compiled module.</em></p> <p><em>This documentation was dynamically generated from the compiled module.</em></p>
<div class="toc"> <div class="toc">
@ -2144,16 +2144,14 @@ Note:</p>
</div> </div>
<div style="margin-left: 20px; margin-bottom: 15px;"> <div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">visible_entities(fov=None, radius: int = -1) -> list[Entity]</code></h5> <h5><code class="method-name">visible_entities(fov=None, radius: int | None = None) -> list[Entity]</code></h5>
<p>Get list of other entities visible from this entity&#x27;s position. <p>Get list of other entities visible from this entity&#x27;s position.</p>
Note:</p>
<div style='margin-left: 20px;'> <div style='margin-left: 20px;'>
<div><span class='arg-name'>fov</span>: FOV algorithm to use (FOV enum or None to use grid.fov)</div> <div><span class='arg-name'>fov</span>: FOV algorithm to use (FOV enum or None to use grid.fov)</div>
<div><span class='arg-name'>radius</span>: FOV radius as int; omit or pass -1 to use the grid&#x27;s default fov_radius</div> <div><span class='arg-name'>radius</span>: FOV radius as int; pass None, omit, or pass -1 to use the grid&#x27;s default fov_radius</div>
</div> </div>
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> List of Entity objects within field of view, excluding self</p> <p style='margin-left: 20px;'><span class='returns'>Returns:</span> List of Entity objects within field of view, excluding self</p>
<p style='margin-left: 20px;'><span class='raises'>Raises:</span> ValueError: If entity is not associated with a grid radius does not accept None; omit the argument entirely to use the grid default.</p> <p style='margin-left: 20px;'><span class='raises'>Raises:</span> ValueError: If entity is not associated with a grid TypeError: If radius is neither an int nor None</p>
</div> </div>
</div> </div>
@ -2619,7 +2617,7 @@ Keyword Args:
<li><span class='property-name'>rotate_with_camera</span>: Whether to rotate visually with parent Grid&#x27;s camera_rotation (bool). False (default): stay screen-aligned. True: tilt with camera. Only affects children of UIGrid; ignored for other parents.</li> <li><span class='property-name'>rotate_with_camera</span>: Whether to rotate visually with parent Grid&#x27;s camera_rotation (bool). False (default): stay screen-aligned. True: tilt with camera. Only affects children of UIGrid; ignored for other parents.</li>
<li><span class='property-name'>rotation</span>: Rotation angle in degrees (clockwise around origin). Animatable property.</li> <li><span class='property-name'>rotation</span>: Rotation angle in degrees (clockwise around origin). Animatable property.</li>
<li><span class='property-name'>shader</span>: Shader for GPU visual effects (Shader or None). When set, the drawable is rendered through the shader program. Set to None to disable shader effects.</li> <li><span class='property-name'>shader</span>: Shader for GPU visual effects (Shader or None). When set, the drawable is rendered through the shader program. Set to None to disable shader effects.</li>
<li><span class='property-name'>texture</span> (read-only): Texture used for tile rendering (None, read-only). Texture return is not yet implemented; always returns None.</li> <li><span class='property-name'>texture</span> (read-only): Texture used for tile rendering (Texture | None, read-only).</li>
<li><span class='property-name'>uniforms</span> (read-only): Collection of shader uniforms (read-only access to collection). Set uniforms via dict-like syntax: drawable.uniforms[&#x27;name&#x27;] = value. Supports float, vec2/3/4 tuples, PropertyBinding, and CallableBinding.</li> <li><span class='property-name'>uniforms</span> (read-only): Collection of shader uniforms (read-only access to collection). Set uniforms via dict-like syntax: drawable.uniforms[&#x27;name&#x27;] = value. Supports float, vec2/3/4 tuples, PropertyBinding, and CallableBinding.</li>
<li><span class='property-name'>vert_margin</span>: Vertical margin override (float, 0 = use general margin). Invalid for horizontally-centered alignments (CENTER_LEFT, CENTER_RIGHT, CENTER).</li> <li><span class='property-name'>vert_margin</span>: Vertical margin override (float, 0 = use general margin). Invalid for horizontally-centered alignments (CENTER_LEFT, CENTER_RIGHT, CENTER).</li>
<li><span class='property-name'>visible</span>: Whether the object is visible (bool). Invisible objects are not rendered or clickable.</li> <li><span class='property-name'>visible</span>: Whether the object is visible (bool). Invisible objects are not rendered or clickable.</li>
@ -2741,7 +2739,7 @@ Keyword Args:
<li><span class='property-name'>rotate_with_camera</span>: Whether to rotate visually with parent Grid&#x27;s camera_rotation (bool). False (default): stay screen-aligned. True: tilt with camera. Only affects children of UIGrid; ignored for other parents.</li> <li><span class='property-name'>rotate_with_camera</span>: Whether to rotate visually with parent Grid&#x27;s camera_rotation (bool). False (default): stay screen-aligned. True: tilt with camera. Only affects children of UIGrid; ignored for other parents.</li>
<li><span class='property-name'>rotation</span>: Rotation angle in degrees (clockwise around origin). Animatable property.</li> <li><span class='property-name'>rotation</span>: Rotation angle in degrees (clockwise around origin). Animatable property.</li>
<li><span class='property-name'>shader</span>: Shader for GPU visual effects (Shader or None). When set, the drawable is rendered through the shader program. Set to None to disable shader effects.</li> <li><span class='property-name'>shader</span>: Shader for GPU visual effects (Shader or None). When set, the drawable is rendered through the shader program. Set to None to disable shader effects.</li>
<li><span class='property-name'>texture</span> (read-only): Texture used for tile rendering (None, read-only). Texture return is not yet implemented; always returns None.</li> <li><span class='property-name'>texture</span> (read-only): Texture used for tile rendering (Texture | None, read-only).</li>
<li><span class='property-name'>uniforms</span> (read-only): Collection of shader uniforms (read-only access to collection). Set uniforms via dict-like syntax: drawable.uniforms[&#x27;name&#x27;] = value. Supports float, vec2/3/4 tuples, PropertyBinding, and CallableBinding.</li> <li><span class='property-name'>uniforms</span> (read-only): Collection of shader uniforms (read-only access to collection). Set uniforms via dict-like syntax: drawable.uniforms[&#x27;name&#x27;] = value. Supports float, vec2/3/4 tuples, PropertyBinding, and CallableBinding.</li>
<li><span class='property-name'>vert_margin</span>: Vertical margin override (float, 0 = use general margin). Invalid for horizontally-centered alignments (CENTER_LEFT, CENTER_RIGHT, CENTER).</li> <li><span class='property-name'>vert_margin</span>: Vertical margin override (float, 0 = use general margin). Invalid for horizontally-centered alignments (CENTER_LEFT, CENTER_RIGHT, CENTER).</li>
<li><span class='property-name'>visible</span>: Whether the object is visible (bool). Invisible objects are not rendered or clickable.</li> <li><span class='property-name'>visible</span>: Whether the object is visible (bool). Invisible objects are not rendered or clickable.</li>

View file

@ -14,11 +14,11 @@
. ftr VB CB . ftr VB CB
. ftr VBI CBI . ftr VBI CBI
.\} .\}
.TH "MCRFPY" "3" "2026-06-21" "McRogueFace 0.2.7-prerelease-7drl2026-107-g3f39ee0" "" .TH "MCRFPY" "3" "2026-06-21" "McRogueFace 0.2.7-prerelease-7drl2026-108-g2654253" ""
.hy .hy
.SH McRogueFace API Reference .SH McRogueFace API Reference
.PP .PP
\f[I]Generated on 2026-06-21 09:39:04\f[R] \f[I]Generated on 2026-06-21 10:11:50\f[R]
.PP .PP
\f[I]This documentation was dynamically generated from the compiled \f[I]This documentation was dynamically generated from the compiled
module.\f[R] module.\f[R]
@ -2201,22 +2201,19 @@ Note:
.PP .PP
\f[B]Returns:\f[R] None Called automatically when the entity moves if \f[B]Returns:\f[R] None Called automatically when the entity moves if
the grid has FOV configured. the grid has FOV configured.
.SS \f[V]visible_entities(fov=None, radius: int = -1) -> list[Entity]\f[R] .SS \f[V]visible_entities(fov=None, radius: int | None = None) -> list[Entity]\f[R]
.PP .PP
Get list of other entities visible from this entity\[cq]s position. Get list of other entities visible from this entity\[cq]s position.
.PP .PP
Note:
.PP
\f[B]Arguments:\f[R] - \f[V]fov\f[R]: FOV algorithm to use (FOV enum or \f[B]Arguments:\f[R] - \f[V]fov\f[R]: FOV algorithm to use (FOV enum or
None to use grid.fov) - \f[V]radius\f[R]: FOV radius as int; omit or None to use grid.fov) - \f[V]radius\f[R]: FOV radius as int; pass None,
pass -1 to use the grid\[cq]s default fov_radius omit, or pass -1 to use the grid\[cq]s default fov_radius
.PP .PP
\f[B]Returns:\f[R] List of Entity objects within field of view, \f[B]Returns:\f[R] List of Entity objects within field of view,
excluding self excluding self
.PP .PP
\f[B]Raises:\f[R] ValueError: If entity is not associated with a grid \f[B]Raises:\f[R] ValueError: If entity is not associated with a grid
radius does not accept None; omit the argument entirely to use the grid TypeError: If radius is neither an int nor None
default.
.SS Entity3D .SS Entity3D
.PP .PP
Entity3D(pos=None, **kwargs) Entity3D(pos=None, **kwargs)
@ -2724,8 +2721,7 @@ Animatable property.
When set, the drawable is rendered through the shader program. When set, the drawable is rendered through the shader program.
Set to None to disable shader effects. Set to None to disable shader effects.
- \f[V]texture\f[R] \f[I](read-only)\f[R]: Texture used for tile - \f[V]texture\f[R] \f[I](read-only)\f[R]: Texture used for tile
rendering (None, read-only). rendering (Texture | None, read-only).
Texture return is not yet implemented; always returns None.
- \f[V]uniforms\f[R] \f[I](read-only)\f[R]: Collection of shader - \f[V]uniforms\f[R] \f[I](read-only)\f[R]: Collection of shader
uniforms (read-only access to collection). uniforms (read-only access to collection).
Set uniforms via dict-like syntax: drawable.uniforms[`name'] = value. Set uniforms via dict-like syntax: drawable.uniforms[`name'] = value.
@ -2889,8 +2885,7 @@ Animatable property.
When set, the drawable is rendered through the shader program. When set, the drawable is rendered through the shader program.
Set to None to disable shader effects. Set to None to disable shader effects.
- \f[V]texture\f[R] \f[I](read-only)\f[R]: Texture used for tile - \f[V]texture\f[R] \f[I](read-only)\f[R]: Texture used for tile
rendering (None, read-only). rendering (Texture | None, read-only).
Texture return is not yet implemented; always returns None.
- \f[V]uniforms\f[R] \f[I](read-only)\f[R]: Collection of shader - \f[V]uniforms\f[R] \f[I](read-only)\f[R]: Collection of shader
uniforms (read-only access to collection). uniforms (read-only access to collection).
Set uniforms via dict-like syntax: drawable.uniforms[`name'] = value. Set uniforms via dict-like syntax: drawable.uniforms[`name'] = value.

View file

@ -114,7 +114,7 @@ sf::Keyboard::Key McRFPy_Automation::stringToKey(const std::string& keyName) {
} }
// Inject mouse event into the game engine // Inject mouse event into the game engine
void McRFPy_Automation::injectMouseEvent(sf::Event::EventType type, int x, int y, sf::Mouse::Button button) { void McRFPy_Automation::injectMouseEvent(sf::Event::EventType type, int x, int y, sf::Mouse::Button button, float scrollDelta) {
auto engine = getGameEngine(); auto engine = getGameEngine();
if (!engine) return; if (!engine) return;
@ -141,8 +141,8 @@ void McRFPy_Automation::injectMouseEvent(sf::Event::EventType type, int x, int y
break; break;
case sf::Event::MouseWheelScrolled: case sf::Event::MouseWheelScrolled:
event.mouseWheelScroll.wheel = sf::Mouse::VerticalWheel; event.mouseWheelScroll.wheel = sf::Mouse::VerticalWheel;
event.mouseWheelScroll.delta = static_cast<float>(x); // x is used for scroll amount event.mouseWheelScroll.delta = scrollDelta; // #317: scroll amount is its own arg
event.mouseWheelScroll.x = x; event.mouseWheelScroll.x = x; // position now honored on both axes
event.mouseWheelScroll.y = y; event.mouseWheelScroll.y = y;
break; break;
default: default:
@ -600,8 +600,9 @@ PyObject* McRFPy_Automation::_scroll(PyObject* self, PyObject* args, PyObject* k
} }
} }
// Inject scroll event // Inject scroll event (#317: forward the resolved x/y position; clicks is
injectMouseEvent(sf::Event::MouseWheelScrolled, clicks, y); // the scroll delta, passed via its own argument).
injectMouseEvent(sf::Event::MouseWheelScrolled, x, y, sf::Mouse::Left, static_cast<float>(clicks));
Py_RETURN_NONE; Py_RETURN_NONE;
} }
@ -953,7 +954,7 @@ static PyMethodDef automationMethods[] = {
MCRF_ARGS_START MCRF_ARGS_START
MCRF_ARG("clicks", "Number of scroll steps (positive = up, negative = down)") MCRF_ARG("clicks", "Number of scroll steps (positive = up, negative = down)")
MCRF_ARG("pos", "Position as (x, y) tuple, [x, y] list, Vector, or None for current position") MCRF_ARG("pos", "Position as (x, y) tuple, [x, y] list, Vector, or None for current position")
MCRF_NOTE("The x-coordinate of pos is currently unused; only the y-coordinate is applied to the scroll event position.") MCRF_NOTE("Both the x and y of pos are applied to the scroll event position.")
)}, )},
{"mouseDown", (PyCFunction)McRFPy_Automation::_mouseDown, METH_VARARGS | METH_KEYWORDS, {"mouseDown", (PyCFunction)McRFPy_Automation::_mouseDown, METH_VARARGS | METH_KEYWORDS,
MCRF_METHOD(automation, mouseDown, MCRF_METHOD(automation, mouseDown,

View file

@ -43,7 +43,7 @@ public:
static PyObject* _keyUp(PyObject* self, PyObject* args); static PyObject* _keyUp(PyObject* self, PyObject* args);
// Helper functions // Helper functions
static void injectMouseEvent(sf::Event::EventType type, int x, int y, sf::Mouse::Button button = sf::Mouse::Left); static void injectMouseEvent(sf::Event::EventType type, int x, int y, sf::Mouse::Button button = sf::Mouse::Left, float scrollDelta = 0.0f);
static void injectKeyEvent(sf::Event::EventType type, sf::Keyboard::Key key); static void injectKeyEvent(sf::Event::EventType type, sf::Keyboard::Key key);
static void injectTextEvent(sf::Uint32 unicode); static void injectTextEvent(sf::Uint32 unicode);
static sf::Keyboard::Key stringToKey(const std::string& keyName); static sf::Keyboard::Key stringToKey(const std::string& keyName);

View file

@ -1315,13 +1315,26 @@ PyObject* UIEntity::visible_entities(PyUIEntityObject* self, PyObject* args, PyO
{ {
static const char* keywords[] = {"fov", "radius", nullptr}; static const char* keywords[] = {"fov", "radius", nullptr};
PyObject* fov_arg = nullptr; PyObject* fov_arg = nullptr;
PyObject* radius_arg = nullptr;
int radius = -1; // -1 means use grid default int radius = -1; // -1 means use grid default
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|Oi", const_cast<char**>(keywords), if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", const_cast<char**>(keywords),
&fov_arg, &radius)) { &fov_arg, &radius_arg)) {
return NULL; return NULL;
} }
// #319: radius accepts an int, or None / omitted to use the grid default.
// The 'i' format code rejects None, so parse as an object and convert here.
if (radius_arg && radius_arg != Py_None) {
if (!PyLong_Check(radius_arg)) {
PyErr_SetString(PyExc_TypeError,
"visible_entities() radius must be an int or None");
return NULL;
}
radius = static_cast<int>(PyLong_AsLong(radius_arg));
if (radius == -1 && PyErr_Occurred()) return NULL;
}
// Check if entity has a grid // Check if entity has a grid
if (!self->data || !self->data->grid) { if (!self->data || !self->data->grid) {
PyErr_SetString(PyExc_ValueError, "Entity must be associated with a grid to find visible entities"); PyErr_SetString(PyExc_ValueError, "Entity must be associated with a grid to find visible entities");
@ -1449,14 +1462,14 @@ PyMethodDef UIEntity::methods[] = {
)}, )},
{"visible_entities", (PyCFunction)UIEntity::visible_entities, METH_VARARGS | METH_KEYWORDS, {"visible_entities", (PyCFunction)UIEntity::visible_entities, METH_VARARGS | METH_KEYWORDS,
MCRF_METHOD(Entity, visible_entities, MCRF_METHOD(Entity, visible_entities,
MCRF_SIG("(fov=None, radius: int = -1)", "list[Entity]"), MCRF_SIG("(fov=None, radius: int | None = None)", "list[Entity]"),
MCRF_DESC("Get list of other entities visible from this entity's position."), MCRF_DESC("Get list of other entities visible from this entity's position."),
MCRF_ARGS_START MCRF_ARGS_START
MCRF_ARG("fov", "FOV algorithm to use (FOV enum or None to use grid.fov)") MCRF_ARG("fov", "FOV algorithm to use (FOV enum or None to use grid.fov)")
MCRF_ARG("radius", "FOV radius as int; omit or pass -1 to use the grid's default fov_radius") MCRF_ARG("radius", "FOV radius as int; pass None, omit, or pass -1 to use the grid's default fov_radius")
MCRF_RETURNS("List of Entity objects within field of view, excluding self") MCRF_RETURNS("List of Entity objects within field of view, excluding self")
MCRF_RAISES("ValueError", "If entity is not associated with a grid") MCRF_RAISES("ValueError", "If entity is not associated with a grid")
MCRF_NOTE("radius does not accept None; omit the argument entirely to use the grid default.") MCRF_RAISES("TypeError", "If radius is neither an int nor None")
)}, )},
{NULL, NULL, 0, NULL} {NULL, NULL, 0, NULL}
}; };
@ -1859,14 +1872,14 @@ PyMethodDef UIEntity_all_methods[] = {
)}, )},
{"visible_entities", (PyCFunction)UIEntity::visible_entities, METH_VARARGS | METH_KEYWORDS, {"visible_entities", (PyCFunction)UIEntity::visible_entities, METH_VARARGS | METH_KEYWORDS,
MCRF_METHOD(Entity, visible_entities, MCRF_METHOD(Entity, visible_entities,
MCRF_SIG("(fov=None, radius: int = -1)", "list[Entity]"), MCRF_SIG("(fov=None, radius: int | None = None)", "list[Entity]"),
MCRF_DESC("Get list of other entities visible from this entity's position."), MCRF_DESC("Get list of other entities visible from this entity's position."),
MCRF_ARGS_START MCRF_ARGS_START
MCRF_ARG("fov", "FOV algorithm to use (FOV enum or None to use grid.fov)") MCRF_ARG("fov", "FOV algorithm to use (FOV enum or None to use grid.fov)")
MCRF_ARG("radius", "FOV radius as int; omit or pass -1 to use the grid's default fov_radius") MCRF_ARG("radius", "FOV radius as int; pass None, omit, or pass -1 to use the grid's default fov_radius")
MCRF_RETURNS("List of Entity objects within field of view, excluding self") MCRF_RETURNS("List of Entity objects within field of view, excluding self")
MCRF_RAISES("ValueError", "If entity is not associated with a grid") MCRF_RAISES("ValueError", "If entity is not associated with a grid")
MCRF_NOTE("radius does not accept None; omit the argument entirely to use the grid default.") MCRF_RAISES("TypeError", "If radius is neither an int nor None")
)}, )},
// #296 - Label methods // #296 - Label methods
{"add_label", (PyCFunction)UIEntity::py_add_label, METH_O, {"add_label", (PyCFunction)UIEntity::py_add_label, METH_O,

View file

@ -8,6 +8,7 @@
#include "Resources.h" #include "Resources.h"
#include "Profiler.h" #include "Profiler.h"
#include "PyShader.h" #include "PyShader.h"
#include "PyTexture.h"
#include "PyUniformCollection.h" #include "PyUniformCollection.h"
#include "PyPositionHelper.h" #include "PyPositionHelper.h"
#include "PyVector.h" #include "PyVector.h"
@ -771,9 +772,16 @@ int UIGridView::set_fill_color(PyUIGridViewObject* self, PyObject* value, void*
PyObject* UIGridView::get_texture(PyUIGridViewObject* self, void* closure) PyObject* UIGridView::get_texture(PyUIGridViewObject* self, void* closure)
{ {
if (!self->data->ptex) Py_RETURN_NONE; // #318: return a Texture wrapper sharing the underlying shared_ptr<PyTexture>,
// TODO: return texture wrapper // mirroring UIGrid::get_texture. None only when the view has no texture.
Py_RETURN_NONE; auto& texture = self->data->ptex;
if (!texture) Py_RETURN_NONE;
auto type = &mcrfpydef::PyTextureType;
auto obj = (PyTextureObject*)type->tp_alloc(type, 0);
if (!obj) return NULL;
obj->data = texture;
return (PyObject*)obj;
} }
// Float member getters/setters for GridView-specific float members (center_x, center_y, zoom, camera_rotation) // Float member getters/setters for GridView-specific float members (center_x, center_y, zoom, camera_rotation)
@ -893,8 +901,10 @@ PyGetSetDef UIGridView::getsetters[] = {
MCRF_PROPERTY(zoom, "Zoom level for rendering (float). Values greater than 1.0 magnify; less than 1.0 shrink."), NULL}, MCRF_PROPERTY(zoom, "Zoom level for rendering (float). Values greater than 1.0 magnify; less than 1.0 shrink."), NULL},
{"fill_color", (getter)UIGridView::get_fill_color, (setter)UIGridView::set_fill_color, {"fill_color", (getter)UIGridView::get_fill_color, (setter)UIGridView::set_fill_color,
MCRF_PROPERTY(fill_color, "Background fill color (Color). Drawn behind all tiles and entities."), NULL}, MCRF_PROPERTY(fill_color, "Background fill color (Color). Drawn behind all tiles and entities."), NULL},
// #318/#252: this type is exposed as BOTH mcrfpy.Grid and mcrfpy.GridView, so the
// docstring is kept type-neutral (accurate for either name).
{"texture", (getter)UIGridView::get_texture, NULL, {"texture", (getter)UIGridView::get_texture, NULL,
MCRF_PROPERTY(texture, "Texture used for tile rendering (None, read-only). Texture return is not yet implemented; always returns None."), NULL}, MCRF_PROPERTY(texture, "Texture used for tile rendering (Texture | None, read-only)."), NULL},
// UIDrawable base properties - applied to GridView (the rendered object) // UIDrawable base properties - applied to GridView (the rendered object)
{"pos", (getter)UIDrawable::get_pos, (setter)UIDrawable::set_pos, {"pos", (getter)UIDrawable::get_pos, (setter)UIDrawable::set_pos,
MCRF_PROPERTY(pos, "Position of the grid as Vector (Vector)."), (void*)PyObjectsEnum::UIGRIDVIEW}, MCRF_PROPERTY(pos, "Position of the grid as Vector (Vector)."), (void*)PyObjectsEnum::UIGRIDVIEW},

View file

@ -725,7 +725,7 @@ class Entity:
def update_visibility(self) -> None: def update_visibility(self) -> None:
"""Recompute which cells are visible from this entity's position and update perspective_map.""" """Recompute which cells are visible from this entity's position and update perspective_map."""
... ...
def visible_entities(self, fov=None, radius: int = -1) -> list[Entity]: def visible_entities(self, fov=None, radius: int | None = None) -> list[Entity]:
"""Get list of other entities visible from this entity's position.""" """Get list of other entities visible from this entity's position."""
... ...
@ -903,7 +903,7 @@ class Grid:
rotation: Any # Rotation angle in degrees (clockwise around origin). Animatable property. rotation: Any # Rotation angle in degrees (clockwise around origin). Animatable property.
shader: Any # Shader for GPU visual effects (Shader or None). When set, the drawable is rendered through the shader program. Set to None to disable shader effects. shader: Any # Shader for GPU visual effects (Shader or None). When set, the drawable is rendered through the shader program. Set to None to disable shader effects.
size: Vector # Size of the grid widget as Vector (Vector, width x height in pixels). size: Vector # Size of the grid widget as Vector (Vector, width x height in pixels).
texture: None # Texture used for tile rendering (None, read-only). Texture return is not yet implemented; always returns None. texture: Texture | None # Texture used for tile rendering (Texture | None, read-only).
uniforms: Any # Collection of shader uniforms (read-only access to collection). Set uniforms via dict-like syntax: drawable.uniforms['name'] = value. Supports float, vec2/3/... uniforms: Any # Collection of shader uniforms (read-only access to collection). Set uniforms via dict-like syntax: drawable.uniforms['name'] = value. Supports float, vec2/3/...
vert_margin: float # Vertical margin override (float, 0 = use general margin). Invalid for horizontally-centered alignments (CENTER_LEFT, CENTER_RIGHT, CENTER). vert_margin: float # Vertical margin override (float, 0 = use general margin). Invalid for horizontally-centered alignments (CENTER_LEFT, CENTER_RIGHT, CENTER).
view: GridView | None # Auto-created GridView for rendering (GridView | None, read-only). When Grid is appended to a scene, this view is what actually renders. view: GridView | None # Auto-created GridView for rendering (GridView | None, read-only). When Grid is appended to a scene, this view is what actually renders.
@ -1013,7 +1013,7 @@ class GridView:
rotation: Any # Rotation angle in degrees (clockwise around origin). Animatable property. rotation: Any # Rotation angle in degrees (clockwise around origin). Animatable property.
shader: Any # Shader for GPU visual effects (Shader or None). When set, the drawable is rendered through the shader program. Set to None to disable shader effects. shader: Any # Shader for GPU visual effects (Shader or None). When set, the drawable is rendered through the shader program. Set to None to disable shader effects.
size: Vector # Size of the grid widget as Vector (Vector, width x height in pixels). size: Vector # Size of the grid widget as Vector (Vector, width x height in pixels).
texture: None # Texture used for tile rendering (None, read-only). Texture return is not yet implemented; always returns None. texture: Texture | None # Texture used for tile rendering (Texture | None, read-only).
uniforms: Any # Collection of shader uniforms (read-only access to collection). Set uniforms via dict-like syntax: drawable.uniforms['name'] = value. Supports float, vec2/3/... uniforms: Any # Collection of shader uniforms (read-only access to collection). Set uniforms via dict-like syntax: drawable.uniforms['name'] = value. Supports float, vec2/3/...
vert_margin: float # Vertical margin override (float, 0 = use general margin). Invalid for horizontally-centered alignments (CENTER_LEFT, CENTER_RIGHT, CENTER). vert_margin: float # Vertical margin override (float, 0 = use general margin). Invalid for horizontally-centered alignments (CENTER_LEFT, CENTER_RIGHT, CENTER).
view: GridView | None # Auto-created GridView for rendering (GridView | None, read-only). When Grid is appended to a scene, this view is what actually renders. view: GridView | None # Auto-created GridView for rendering (GridView | None, read-only). When Grid is appended to a scene, this view is what actually renders.

View file

@ -0,0 +1,68 @@
"""
Regression test for issue #317 -- automation.scroll() ignored the x-coordinate
of its position argument.
Root cause: scroll() resolved (x, y) from pos but called
injectMouseEvent(MouseWheelScrolled, clicks, y)
passing `clicks` as the x argument, so the resolved x was dropped AND the event's
mouseWheelScroll.x was set to the scroll amount. The fix gives the scroll delta
its own injectMouseEvent parameter and forwards the real x/y.
LIMITATION: no Python-observable handler currently consumes a scroll event's
position (GameEngine maps only the wheel *delta* to a wheel_up/wheel_down action),
so this test cannot assert the forwarded x end-to-end. It is therefore a smoke /
API-contract regression guard: scroll(clicks, pos=(x, y)) must accept any x, all
documented pos forms, and not raise after the injectMouseEvent refactor (which is
the part that changed how the scroll delta is passed). The x-forwarding itself is
verified by code inspection (McRFPy_Automation.cpp scroll/injectMouseEvent).
ASCII-only. Prints PASS/FAIL + exit.
"""
import mcrfpy
from mcrfpy import automation
import sys
failures = []
def check(label, fn):
try:
fn()
ok = True
except Exception as e:
ok = False
label = "%s (raised %s: %s)" % (label, type(e).__name__, e)
if not ok:
failures.append(label)
print((" ok " if ok else " FAIL ") + label)
# A scene must be current for event injection to have a target.
scene = mcrfpy.Scene("issue317")
mcrfpy.current_scene = scene
# Vary x across the position argument; previously x was silently dropped.
check("scroll(3) no position", lambda: automation.scroll(3))
check("scroll(3, pos=None)", lambda: automation.scroll(3, pos=None))
check("scroll(3, (100, 50)) tuple x!=0", lambda: automation.scroll(3, (100, 50)))
check("scroll(-2, (0, 50)) x==0", lambda: automation.scroll(-2, (0, 50)))
check("scroll(1, [250, 75]) list", lambda: automation.scroll(1, [250, 75]))
check("scroll(5, pos=(640, 480)) keyword", lambda: automation.scroll(5, pos=(640, 480)))
check("scroll(-5, mcrfpy.Vector(12, 34)) Vector",
lambda: automation.scroll(-5, mcrfpy.Vector(12, 34)))
# Same y, different x must both be accepted (the regression dropped x entirely).
check("scroll(2, (10, 200))", lambda: automation.scroll(2, (10, 200)))
check("scroll(2, (900, 200))", lambda: automation.scroll(2, (900, 200)))
print("")
if failures:
print("FAIL -- %d check(s) failed:" % len(failures))
for f in failures:
print(" - " + f)
sys.exit(1)
print("PASS -- automation.scroll accepts a full (x, y) position across all forms "
"(x-forwarding verified by code inspection; no observable consumer yet).")
sys.exit(0)

View file

@ -0,0 +1,70 @@
"""
Regression test for issue #318 -- GridView.texture always returned None.
Root cause: the getter had a `TODO: return texture wrapper` and fell through to
Py_RETURN_NONE even when self->data->ptex was non-null. The fix returns a
Texture wrapper sharing the underlying shared_ptr<PyTexture>, mirroring
UIGrid.texture.
Needs an asset; run via run_tests.py (cwd=build/) or from build/ so that
assets/kenney_tinydungeon.png resolves. ASCII-only. Prints PASS/FAIL + exit.
"""
import mcrfpy
import sys
failures = []
def check(label, cond):
if not cond:
failures.append(label)
print((" ok " if cond else " FAIL ") + label)
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
# --- GridView over a textured grid: texture must now be returned -----------
grid = mcrfpy.Grid(grid_size=(10, 10), texture=texture)
view = mcrfpy.GridView(grid=grid, pos=(0, 0), size=(160, 160))
t = view.texture
check("GridView.texture is not None when the view has a texture", t is not None)
check("GridView.texture is a Texture", isinstance(t, mcrfpy.Texture))
if isinstance(t, mcrfpy.Texture):
# Wrapper should describe the same underlying sheet as the source.
check("sprite_width matches source (16)", t.sprite_width == 16)
check("sprite_height matches source (16)", t.sprite_height == 16)
check("matches grid.texture sprite dims",
grid.texture is not None
and t.sprite_width == grid.texture.sprite_width
and t.sprite_height == grid.texture.sprite_height)
# --- A grid created without an explicit texture still gets the engine default
# texture, so its view's texture is also non-None (the getter's None branch is
# defensive-only -- no public API produces a textureless grid). -------------
bare_grid = mcrfpy.Grid(grid_size=(8, 8))
bare_view = mcrfpy.GridView(grid=bare_grid, pos=(0, 0), size=(80, 80))
check("default-textured grid's view exposes a Texture (not None)",
isinstance(bare_view.texture, mcrfpy.Texture))
check("view texture matches its grid's texture source",
bare_view.texture.source == bare_grid.texture.source)
# --- property is read-only (NULL setter) -----------------------------------
try:
view.texture = texture
check("GridView.texture is read-only (assignment raises)", False)
except AttributeError:
check("GridView.texture is read-only (assignment raises)", True)
except Exception as e:
check("GridView.texture is read-only (raised %s)" % type(e).__name__, False)
print("")
if failures:
print("FAIL -- %d check(s) failed:" % len(failures))
for f in failures:
print(" - " + f)
sys.exit(1)
print("PASS -- GridView.texture returns the tile Texture (or None when absent).")
sys.exit(0)

View file

@ -0,0 +1,95 @@
"""
Regression test for issue #319 -- Entity.visible_entities(radius=None) raised
TypeError instead of applying the grid default.
Root cause: radius was parsed with the PyArg 'i' format code ("|Oi"), which
rejects Python None. The fix parses radius as an object ("|OO") and treats
None / omitted / -1 as "use the grid's default fov_radius".
ASCII-only source. Prints PASS/FAIL and sys.exit(0/1).
"""
import mcrfpy
import sys
failures = []
def check(label, cond):
if not cond:
failures.append(label)
print((" ok " if cond else " FAIL ") + label)
# --- grid + entities -------------------------------------------------------
grid = mcrfpy.Grid(grid_size=(20, 20))
grid.fov_radius = 7
for gx in range(20):
for gy in range(20):
gp = grid.at(gx, gy)
gp.walkable = True
gp.transparent = True
seeker = mcrfpy.Entity(grid_pos=(10, 10))
grid.entities.append(seeker)
neighbor = mcrfpy.Entity(grid_pos=(11, 10))
grid.entities.append(neighbor)
# --- the bug: radius=None must NOT raise (it used to TypeError) -------------
try:
res_none = seeker.visible_entities(radius=None)
check("visible_entities(radius=None) does not raise", True)
check("returns a list", isinstance(res_none, list))
except TypeError as e:
check("visible_entities(radius=None) does not raise (got TypeError: %s)" % e, False)
res_none = []
# Adjacent transparent neighbor should be visible.
def positions(lst):
return {(int(e.cell_pos.x), int(e.cell_pos.y)) for e in lst}
check("neighbor (11,10) visible with radius=None", (11, 10) in positions(res_none))
check("self excluded from results", (10, 10) not in positions(res_none))
# --- equivalence: None, omitted, and -1 all mean 'use grid default' --------
res_omitted = seeker.visible_entities()
res_default = seeker.visible_entities(radius=-1)
check("radius=None matches omitted", positions(res_none) == positions(res_omitted))
check("radius=None matches radius=-1", positions(res_none) == positions(res_default))
# --- explicit int radius still works ---------------------------------------
try:
res_int = seeker.visible_entities(radius=3)
check("radius=3 (int) works", isinstance(res_int, list) and (11, 10) in positions(res_int))
except Exception as e:
check("radius=3 (int) works (raised %s)" % e, False)
# A tiny radius can still see the immediately-adjacent neighbor.
res_one = seeker.visible_entities(radius=1)
check("radius=1 sees adjacent neighbor", (11, 10) in positions(res_one))
# --- fov + radius=None combined (None must be fine alongside fov) -----------
try:
res_combo = seeker.visible_entities(fov=mcrfpy.FOV.SHADOW, radius=None)
check("visible_entities(fov=SHADOW, radius=None) works", isinstance(res_combo, list))
except Exception as e:
check("visible_entities(fov=SHADOW, radius=None) works (raised %s)" % e, False)
# --- invalid radius type must raise a clear TypeError ----------------------
try:
seeker.visible_entities(radius="not an int")
check("radius='str' raises TypeError", False)
except TypeError:
check("radius='str' raises TypeError", True)
except Exception as e:
check("radius='str' raises TypeError (got %s)" % type(e).__name__, False)
print("")
if failures:
print("FAIL -- %d check(s) failed:" % len(failures))
for f in failures:
print(" - " + f)
sys.exit(1)
print("PASS -- visible_entities accepts radius=None (and -1/omitted) as the grid default.")
sys.exit(0)

View file

@ -487,7 +487,7 @@ submodule automation
meth resize :: resize(width, height) or (size) -> None meth resize :: resize(width, height) or (size) -> None
meth set_behavior :: set_behavior(type, waypoints=None, turns: int = 0, path=None, pathfinder=None) -> None meth set_behavior :: set_behavior(type, waypoints=None, turns: int = 0, path=None, pathfinder=None) -> None
meth update_visibility :: update_visibility() -> None meth update_visibility :: update_visibility() -> None
meth visible_entities :: visible_entities(fov=None, radius: int = -1) -> list[Entity] meth visible_entities :: visible_entities(fov=None, radius: int | None = None) -> list[Entity]
[Font] [Font]
prop family: str (ro) prop family: str (ro)
prop source: str (ro) prop source: str (ro)
@ -558,7 +558,7 @@ submodule automation
prop rotate_with_camera: bool (rw) prop rotate_with_camera: bool (rw)
prop rotation: Any (rw) prop rotation: Any (rw)
prop shader: Any (rw) prop shader: Any (rw)
prop texture: None (ro) prop texture: Texture | None (ro)
prop uniforms: Any (ro) prop uniforms: Any (ro)
prop vert_margin: float (rw) prop vert_margin: float (rw)
prop visible: bool (rw) prop visible: bool (rw)
@ -598,7 +598,7 @@ submodule automation
prop rotate_with_camera: bool (rw) prop rotate_with_camera: bool (rw)
prop rotation: Any (rw) prop rotation: Any (rw)
prop shader: Any (rw) prop shader: Any (rw)
prop texture: None (ro) prop texture: Texture | None (ro)
prop uniforms: Any (ro) prop uniforms: Any (ro)
prop vert_margin: float (rw) prop vert_margin: float (rw)
prop visible: bool (rw) prop visible: bool (rw)