From 75127ac9d1fc89ff0a0851864ed8e5f48e17bd33 Mon Sep 17 00:00:00 2001 From: John McCardle Date: Tue, 6 Jan 2026 21:39:01 -0500 Subject: [PATCH] mcrfpy.Mouse: a new class built for symmetry with mcrfpy.Keyboard. Closes #186 --- src/McRFPy_API.cpp | 10 +++ src/PyMouse.cpp | 185 +++++++++++++++++++++++++++++++++++++++++++++ src/PyMouse.h | 50 ++++++++++++ stubs/mcrfpy.pyi | 41 ++++++++++ 4 files changed, 286 insertions(+) create mode 100644 src/PyMouse.cpp create mode 100644 src/PyMouse.h diff --git a/src/McRFPy_API.cpp b/src/McRFPy_API.cpp index 813bea9..0f46054 100644 --- a/src/McRFPy_API.cpp +++ b/src/McRFPy_API.cpp @@ -14,6 +14,7 @@ #include "PySound.h" #include "PyMusic.h" #include "PyKeyboard.h" +#include "PyMouse.h" #include "McRogueFaceVersion.h" #include "GameEngine.h" #include "ImGuiConsole.h" @@ -341,6 +342,9 @@ PyObject* PyInit_mcrfpy() /*keyboard state (#160)*/ &PyKeyboardType, + /*mouse state (#186)*/ + &PyMouseType, + nullptr}; // Types that are used internally but NOT exported to module namespace (#189) @@ -415,6 +419,12 @@ PyObject* PyInit_mcrfpy() PyModule_AddObject(m, "keyboard", keyboard_instance); } + // Add mouse singleton (#186) + PyObject* mouse_instance = PyObject_CallObject((PyObject*)&PyMouseType, NULL); + if (mouse_instance) { + PyModule_AddObject(m, "mouse", mouse_instance); + } + // Add window singleton (#184) // Use tp_alloc directly to bypass tp_new which blocks user instantiation PyObject* window_instance = PyWindowType.tp_alloc(&PyWindowType, 0); diff --git a/src/PyMouse.cpp b/src/PyMouse.cpp new file mode 100644 index 0000000..a450fce --- /dev/null +++ b/src/PyMouse.cpp @@ -0,0 +1,185 @@ +#include "PyMouse.h" +#include "McRFPy_API.h" +#include "McRFPy_Automation.h" +#include "GameEngine.h" +#include "McRFPy_Doc.h" +#include "PyVector.h" + +int PyMouse::init(PyMouseObject* self, PyObject* args, PyObject* kwds) +{ + // Initialize tracked state to SFML defaults + self->cursor_visible = true; + self->cursor_grabbed = false; + return 0; +} + +// Helper to get current mouse position, handling headless mode +static sf::Vector2i getMousePosition() +{ + GameEngine* engine = McRFPy_API::game; + + if (!engine || !engine->getRenderTargetPtr()) { + return McRFPy_Automation::getSimulatedMousePosition(); + } + + if (engine->isHeadless()) { + // In headless mode, return simulated position from automation + return McRFPy_Automation::getSimulatedMousePosition(); + } + + // In windowed mode, get actual mouse position relative to window + if (auto* window = dynamic_cast(engine->getRenderTargetPtr())) { + return sf::Mouse::getPosition(*window); + } + + // Fallback to simulated position + return McRFPy_Automation::getSimulatedMousePosition(); +} + +PyObject* PyMouse::repr(PyObject* obj) +{ + sf::Vector2i pos = getMousePosition(); + bool left = sf::Mouse::isButtonPressed(sf::Mouse::Left); + bool right = sf::Mouse::isButtonPressed(sf::Mouse::Right); + bool middle = sf::Mouse::isButtonPressed(sf::Mouse::Middle); + + PyMouseObject* self = (PyMouseObject*)obj; + + std::ostringstream ss; + ss << ""; + + std::string repr_str = ss.str(); + return PyUnicode_DecodeUTF8(repr_str.c_str(), repr_str.size(), "replace"); +} + +PyObject* PyMouse::get_x(PyObject* self, void* closure) +{ + sf::Vector2i pos = getMousePosition(); + return PyLong_FromLong(pos.x); +} + +PyObject* PyMouse::get_y(PyObject* self, void* closure) +{ + sf::Vector2i pos = getMousePosition(); + return PyLong_FromLong(pos.y); +} + +PyObject* PyMouse::get_pos(PyObject* self, void* closure) +{ + sf::Vector2i pos = getMousePosition(); + + // Return a Vector object + auto vector_type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector"); + if (!vector_type) { + PyErr_SetString(PyExc_RuntimeError, "Vector type not found in mcrfpy module"); + return NULL; + } + PyObject* result = PyObject_CallFunction((PyObject*)vector_type, "ff", (float)pos.x, (float)pos.y); + Py_DECREF(vector_type); + return result; +} + +PyObject* PyMouse::get_left(PyObject* self, void* closure) +{ + bool pressed = sf::Mouse::isButtonPressed(sf::Mouse::Left); + return PyBool_FromLong(pressed); +} + +PyObject* PyMouse::get_right(PyObject* self, void* closure) +{ + bool pressed = sf::Mouse::isButtonPressed(sf::Mouse::Right); + return PyBool_FromLong(pressed); +} + +PyObject* PyMouse::get_middle(PyObject* self, void* closure) +{ + bool pressed = sf::Mouse::isButtonPressed(sf::Mouse::Middle); + return PyBool_FromLong(pressed); +} + +PyObject* PyMouse::get_visible(PyObject* self, void* closure) +{ + PyMouseObject* mouse = (PyMouseObject*)self; + return PyBool_FromLong(mouse->cursor_visible); +} + +int PyMouse::set_visible(PyObject* self, PyObject* value, void* closure) +{ + if (!PyBool_Check(value)) { + PyErr_SetString(PyExc_TypeError, "visible must be a boolean"); + return -1; + } + + PyMouseObject* mouse = (PyMouseObject*)self; + bool visible = PyObject_IsTrue(value); + mouse->cursor_visible = visible; + + // Apply to window if available + GameEngine* engine = McRFPy_API::game; + if (engine && !engine->isHeadless()) { + if (auto* window = dynamic_cast(engine->getRenderTargetPtr())) { + window->setMouseCursorVisible(visible); + } + } + + return 0; +} + +PyObject* PyMouse::get_grabbed(PyObject* self, void* closure) +{ + PyMouseObject* mouse = (PyMouseObject*)self; + return PyBool_FromLong(mouse->cursor_grabbed); +} + +int PyMouse::set_grabbed(PyObject* self, PyObject* value, void* closure) +{ + if (!PyBool_Check(value)) { + PyErr_SetString(PyExc_TypeError, "grabbed must be a boolean"); + return -1; + } + + PyMouseObject* mouse = (PyMouseObject*)self; + bool grabbed = PyObject_IsTrue(value); + mouse->cursor_grabbed = grabbed; + + // Apply to window if available + GameEngine* engine = McRFPy_API::game; + if (engine && !engine->isHeadless()) { + if (auto* window = dynamic_cast(engine->getRenderTargetPtr())) { + window->setMouseCursorGrabbed(grabbed); + } + } + + return 0; +} + +PyGetSetDef PyMouse::getsetters[] = { + // Position (read-only) + {"x", (getter)PyMouse::get_x, NULL, + MCRF_PROPERTY(x, "Current mouse X position in window coordinates (read-only)."), NULL}, + {"y", (getter)PyMouse::get_y, NULL, + MCRF_PROPERTY(y, "Current mouse Y position in window coordinates (read-only)."), NULL}, + {"pos", (getter)PyMouse::get_pos, NULL, + MCRF_PROPERTY(pos, "Current mouse position as Vector (read-only)."), NULL}, + + // Button state (read-only) + {"left", (getter)PyMouse::get_left, NULL, + MCRF_PROPERTY(left, "True if left mouse button is currently pressed (read-only)."), NULL}, + {"right", (getter)PyMouse::get_right, NULL, + MCRF_PROPERTY(right, "True if right mouse button is currently pressed (read-only)."), NULL}, + {"middle", (getter)PyMouse::get_middle, NULL, + MCRF_PROPERTY(middle, "True if middle mouse button is currently pressed (read-only)."), NULL}, + + // Cursor control (read-write) + {"visible", (getter)PyMouse::get_visible, (setter)PyMouse::set_visible, + MCRF_PROPERTY(visible, "Whether the mouse cursor is visible (default: True)."), NULL}, + {"grabbed", (getter)PyMouse::get_grabbed, (setter)PyMouse::set_grabbed, + MCRF_PROPERTY(grabbed, "Whether the mouse cursor is confined to the window (default: False)."), NULL}, + + {NULL} +}; diff --git a/src/PyMouse.h b/src/PyMouse.h new file mode 100644 index 0000000..18aed50 --- /dev/null +++ b/src/PyMouse.h @@ -0,0 +1,50 @@ +#pragma once +#include "Common.h" +#include "Python.h" + +// Singleton mouse state object +typedef struct { + PyObject_HEAD + // Track state for properties without SFML getters + bool cursor_visible; + bool cursor_grabbed; +} PyMouseObject; + +class PyMouse +{ +public: + // Python getters - query real-time mouse state via SFML + static PyObject* get_x(PyObject* self, void* closure); + static PyObject* get_y(PyObject* self, void* closure); + static PyObject* get_pos(PyObject* self, void* closure); + + // Button state getters + static PyObject* get_left(PyObject* self, void* closure); + static PyObject* get_right(PyObject* self, void* closure); + static PyObject* get_middle(PyObject* self, void* closure); + + // Cursor visibility/grab (read-write, tracked internally) + static PyObject* get_visible(PyObject* self, void* closure); + static int set_visible(PyObject* self, PyObject* value, void* closure); + static PyObject* get_grabbed(PyObject* self, void* closure); + static int set_grabbed(PyObject* self, PyObject* value, void* closure); + + static PyObject* repr(PyObject* obj); + static int init(PyMouseObject* self, PyObject* args, PyObject* kwds); + static PyGetSetDef getsetters[]; +}; + +namespace mcrfpydef { + static PyTypeObject PyMouseType = { + .ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0}, + .tp_name = "mcrfpy.Mouse", + .tp_basicsize = sizeof(PyMouseObject), + .tp_itemsize = 0, + .tp_repr = PyMouse::repr, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_doc = PyDoc_STR("Mouse state singleton for reading button/position state and controlling cursor visibility"), + .tp_getset = PyMouse::getsetters, + .tp_init = (initproc)PyMouse::init, + .tp_new = PyType_GenericNew, + }; +} diff --git a/stubs/mcrfpy.pyi b/stubs/mcrfpy.pyi index e685e48..2a890fa 100644 --- a/stubs/mcrfpy.pyi +++ b/stubs/mcrfpy.pyi @@ -204,6 +204,41 @@ class Keyboard: system: bool """True if either System key (Win/Cmd) is currently pressed (read-only).""" +class Mouse: + """Mouse state singleton for reading button/position state and controlling cursor. + + Access via mcrfpy.mouse (singleton instance). + Queries real-time mouse state from SFML. In headless mode, returns + simulated position from mcrfpy.automation calls. + """ + + # Position (read-only) + x: int + """Current mouse X position in window coordinates (read-only).""" + + y: int + """Current mouse Y position in window coordinates (read-only).""" + + pos: Vector + """Current mouse position as Vector (read-only).""" + + # Button state (read-only) + left: bool + """True if left mouse button is currently pressed (read-only).""" + + right: bool + """True if right mouse button is currently pressed (read-only).""" + + middle: bool + """True if middle mouse button is currently pressed (read-only).""" + + # Cursor control (read-write) + visible: bool + """Whether the mouse cursor is visible (default: True).""" + + grabbed: bool + """Whether the mouse cursor is confined to the window (default: False).""" + class Drawable: """Base class for all drawable UI elements.""" @@ -672,6 +707,12 @@ __version__: str keyboard: Keyboard """Keyboard state singleton for checking modifier keys.""" +mouse: Mouse +"""Mouse state singleton for reading button/position state and controlling cursor.""" + +window: Window +"""Window singleton for controlling window properties.""" + # Module functions def sceneUI(scene: Optional[str] = None) -> UICollection: