diff --git a/docs/API_REFERENCE_DYNAMIC.md b/docs/API_REFERENCE_DYNAMIC.md
index 00b6d35..e7fc5e1 100644
--- a/docs/API_REFERENCE_DYNAMIC.md
+++ b/docs/API_REFERENCE_DYNAMIC.md
@@ -1,6 +1,6 @@
# 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.*
@@ -1989,19 +1989,17 @@ Note:
**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.
-Note:
-
**Arguments:**
- `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
-**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
@@ -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.
- `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.
-- `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.
- `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.
@@ -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.
- `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.
-- `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.
- `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.
diff --git a/docs/api_reference_dynamic.html b/docs/api_reference_dynamic.html
index f511245..97c15a2 100644
--- a/docs/api_reference_dynamic.html
+++ b/docs/api_reference_dynamic.html
@@ -108,7 +108,7 @@
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.
@@ -2144,16 +2144,14 @@ Note:
-
visible_entities(fov=None, radius: int = -1) -> list[Entity]
-
Get list of other entities visible from this entity's position.
-
-Note:
+
visible_entities(fov=None, radius: int | None = None) -> list[Entity]
+
Get list of other entities visible from this entity's position.
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
-
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
@@ -2619,7 +2617,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.
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.
- 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.
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.
@@ -2741,7 +2739,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.
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.
- 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.
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.
diff --git a/docs/mcrfpy.3 b/docs/mcrfpy.3
index e390525..8171d95 100644
--- a/docs/mcrfpy.3
+++ b/docs/mcrfpy.3
@@ -14,11 +14,11 @@
. ftr VB CB
. 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
.SH McRogueFace API Reference
.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
\f[I]This documentation was dynamically generated from the compiled
module.\f[R]
@@ -2201,22 +2201,19 @@ Note:
.PP
\f[B]Returns:\f[R] None Called automatically when the entity moves if
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
Get list of other entities visible from this entity\[cq]s position.
.PP
-Note:
-.PP
\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
-pass -1 to use the grid\[cq]s default fov_radius
+None to use grid.fov) - \f[V]radius\f[R]: FOV radius as int; pass None,
+omit, or pass -1 to use the grid\[cq]s default fov_radius
.PP
\f[B]Returns:\f[R] List of Entity objects within field of view,
excluding self
.PP
\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
-default.
+TypeError: If radius is neither an int nor None
.SS Entity3D
.PP
Entity3D(pos=None, **kwargs)
@@ -2724,8 +2721,7 @@ Animatable property.
When set, the drawable is rendered through the shader program.
Set to None to disable shader effects.
- \f[V]texture\f[R] \f[I](read-only)\f[R]: Texture used for tile
-rendering (None, read-only).
-Texture return is not yet implemented; always returns None.
+rendering (Texture | None, read-only).
- \f[V]uniforms\f[R] \f[I](read-only)\f[R]: Collection of shader
uniforms (read-only access to collection).
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.
Set to None to disable shader effects.
- \f[V]texture\f[R] \f[I](read-only)\f[R]: Texture used for tile
-rendering (None, read-only).
-Texture return is not yet implemented; always returns None.
+rendering (Texture | None, read-only).
- \f[V]uniforms\f[R] \f[I](read-only)\f[R]: Collection of shader
uniforms (read-only access to collection).
Set uniforms via dict-like syntax: drawable.uniforms[`name'] = value.
diff --git a/src/McRFPy_Automation.cpp b/src/McRFPy_Automation.cpp
index bf5de2e..0803924 100644
--- a/src/McRFPy_Automation.cpp
+++ b/src/McRFPy_Automation.cpp
@@ -114,7 +114,7 @@ sf::Keyboard::Key McRFPy_Automation::stringToKey(const std::string& keyName) {
}
// 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();
if (!engine) return;
@@ -141,8 +141,8 @@ void McRFPy_Automation::injectMouseEvent(sf::Event::EventType type, int x, int y
break;
case sf::Event::MouseWheelScrolled:
event.mouseWheelScroll.wheel = sf::Mouse::VerticalWheel;
- event.mouseWheelScroll.delta = static_cast(x); // x is used for scroll amount
- event.mouseWheelScroll.x = x;
+ event.mouseWheelScroll.delta = scrollDelta; // #317: scroll amount is its own arg
+ event.mouseWheelScroll.x = x; // position now honored on both axes
event.mouseWheelScroll.y = y;
break;
default:
@@ -600,8 +600,9 @@ PyObject* McRFPy_Automation::_scroll(PyObject* self, PyObject* args, PyObject* k
}
}
- // Inject scroll event
- injectMouseEvent(sf::Event::MouseWheelScrolled, clicks, y);
+ // Inject scroll event (#317: forward the resolved x/y position; clicks is
+ // the scroll delta, passed via its own argument).
+ injectMouseEvent(sf::Event::MouseWheelScrolled, x, y, sf::Mouse::Left, static_cast(clicks));
Py_RETURN_NONE;
}
@@ -953,7 +954,7 @@ static PyMethodDef automationMethods[] = {
MCRF_ARGS_START
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_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,
MCRF_METHOD(automation, mouseDown,
diff --git a/src/McRFPy_Automation.h b/src/McRFPy_Automation.h
index 6d67012..7be6f96 100644
--- a/src/McRFPy_Automation.h
+++ b/src/McRFPy_Automation.h
@@ -43,7 +43,7 @@ public:
static PyObject* _keyUp(PyObject* self, PyObject* args);
// 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 injectTextEvent(sf::Uint32 unicode);
static sf::Keyboard::Key stringToKey(const std::string& keyName);
diff --git a/src/UIEntity.cpp b/src/UIEntity.cpp
index 934b462..f7d1d50 100644
--- a/src/UIEntity.cpp
+++ b/src/UIEntity.cpp
@@ -1315,13 +1315,26 @@ PyObject* UIEntity::visible_entities(PyUIEntityObject* self, PyObject* args, PyO
{
static const char* keywords[] = {"fov", "radius", nullptr};
PyObject* fov_arg = nullptr;
+ PyObject* radius_arg = nullptr;
int radius = -1; // -1 means use grid default
- if (!PyArg_ParseTupleAndKeywords(args, kwds, "|Oi", const_cast(keywords),
- &fov_arg, &radius)) {
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", const_cast(keywords),
+ &fov_arg, &radius_arg)) {
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(PyLong_AsLong(radius_arg));
+ if (radius == -1 && PyErr_Occurred()) return NULL;
+ }
+
// Check if entity has a grid
if (!self->data || !self->data->grid) {
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,
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_ARGS_START
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_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}
};
@@ -1859,14 +1872,14 @@ PyMethodDef UIEntity_all_methods[] = {
)},
{"visible_entities", (PyCFunction)UIEntity::visible_entities, METH_VARARGS | METH_KEYWORDS,
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_ARGS_START
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_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
{"add_label", (PyCFunction)UIEntity::py_add_label, METH_O,
diff --git a/src/UIGridView.cpp b/src/UIGridView.cpp
index a275c6e..e87a43a 100644
--- a/src/UIGridView.cpp
+++ b/src/UIGridView.cpp
@@ -8,6 +8,7 @@
#include "Resources.h"
#include "Profiler.h"
#include "PyShader.h"
+#include "PyTexture.h"
#include "PyUniformCollection.h"
#include "PyPositionHelper.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)
{
- if (!self->data->ptex) Py_RETURN_NONE;
- // TODO: return texture wrapper
- Py_RETURN_NONE;
+ // #318: return a Texture wrapper sharing the underlying shared_ptr,
+ // mirroring UIGrid::get_texture. None only when the view has no texture.
+ 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)
@@ -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},
{"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},
+ // #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,
- 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)
{"pos", (getter)UIDrawable::get_pos, (setter)UIDrawable::set_pos,
MCRF_PROPERTY(pos, "Position of the grid as Vector (Vector)."), (void*)PyObjectsEnum::UIGRIDVIEW},
diff --git a/stubs/mcrfpy.pyi b/stubs/mcrfpy.pyi
index 12482a9..fa99e81 100644
--- a/stubs/mcrfpy.pyi
+++ b/stubs/mcrfpy.pyi
@@ -725,7 +725,7 @@ class Entity:
def update_visibility(self) -> None:
"""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."""
...
@@ -903,7 +903,7 @@ class Grid:
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.
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/...
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.
@@ -1013,7 +1013,7 @@ class GridView:
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.
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/...
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.
diff --git a/tests/regression/issue_317_scroll_position_test.py b/tests/regression/issue_317_scroll_position_test.py
new file mode 100644
index 0000000..f1dea7f
--- /dev/null
+++ b/tests/regression/issue_317_scroll_position_test.py
@@ -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)
diff --git a/tests/regression/issue_318_gridview_texture_test.py b/tests/regression/issue_318_gridview_texture_test.py
new file mode 100644
index 0000000..b9d3485
--- /dev/null
+++ b/tests/regression/issue_318_gridview_texture_test.py
@@ -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, 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)
diff --git a/tests/regression/issue_319_visible_entities_radius_none_test.py b/tests/regression/issue_319_visible_entities_radius_none_test.py
new file mode 100644
index 0000000..15e746a
--- /dev/null
+++ b/tests/regression/issue_319_visible_entities_radius_none_test.py
@@ -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)
diff --git a/tests/snapshots/api_surface.golden.txt b/tests/snapshots/api_surface.golden.txt
index 4646f25..89b030a 100644
--- a/tests/snapshots/api_surface.golden.txt
+++ b/tests/snapshots/api_surface.golden.txt
@@ -487,7 +487,7 @@ submodule automation
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 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]
prop family: str (ro)
prop source: str (ro)
@@ -558,7 +558,7 @@ submodule automation
prop rotate_with_camera: bool (rw)
prop rotation: Any (rw)
prop shader: Any (rw)
- prop texture: None (ro)
+ prop texture: Texture | None (ro)
prop uniforms: Any (ro)
prop vert_margin: float (rw)
prop visible: bool (rw)
@@ -598,7 +598,7 @@ submodule automation
prop rotate_with_camera: bool (rw)
prop rotation: Any (rw)
prop shader: Any (rw)
- prop texture: None (ro)
+ prop texture: Texture | None (ro)
prop uniforms: Any (ro)
prop vert_margin: float (rw)
prop visible: bool (rw)